feat: verify readable SuperAPI history
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

This commit is contained in:
pikastech
2026-07-18 17:52:20 +02:00
parent 7ee59dfaaf
commit 2d31280aef
4 changed files with 130 additions and 1 deletions
@@ -0,0 +1,33 @@
# R2.7.14 任务报告
## 交付结果
- SuperAPI 请求详情增加同一个 `分块 / Raw` 模式控件,Request 与 Response 始终同步切换。
- Request 分块解析请求概要、System Prompt、Developer Prompt、User Prompt、Skills 和 ToolsSuperAPI 注入的 `Company Skills` catalog 从 system prompt 中分离。
- 每个 Skill 和 Tool 独占一行摘要,长内容单行省略,展开后显示完整脱敏字段。
- Response 同时支持同步 JSON 与 SSE,流式响应提取最终 `response.completed`、输出、错误和 Token 用量;Raw 模式保留完整脱敏 SSE。
- API Key 绑定下拉框增加稳定最小宽度和右侧留白,占位符完整显示。
- SuperAPI 提交:`2f68c99 feat: add readable request history details`
## L0 验证
`bun scripts/superapi-cli.ts management l0-smoke` 通过。fixture 实际覆盖 PostgreSQL 历史导入、注入后 skill catalog、user prompt、function tool、SSE `response.completed`、assistant 输出和 Token 用量;`HISTORYREADABLEPARSE=true``HISTORYREADABLERENDER=true`
`bunx vite build` 通过。仓库总 `check` 的后端 bundle 仍受既有 Temporal `swc-loader` 可选依赖解析影响,本任务没有扩大到该无关构建链;目标前端构建、L0 与 L1 均已通过。
## L1 验证
从固定入口 `https://superapi.hwpod.com` 执行 typed `web-probe product-smoke` 通过:
- API Key 弹窗占位符为“请选择要绑定的 Skill”,实测 select 宽度 835px。
- Request 识别 `systemPrompt``userPrompts``skills``tools`
- Response 识别 `overview``outputs``usage`
- 首个 Skill 与 Tool 均可展开且至少显示 2 个详情字段。
- 切到 Raw 后同时出现 2 个 Raw 面板且分块面板消失;切回后分块双栏同时恢复。
- console error 为 0,失败断言为 0,管理写请求为 0。
- 报告 SHA-256`1f06eaa48acb30940e9430a7dbf8ae0f7576d1c05656b33bcca692ce82412a67`
- 分块详情截图 SHA-256`05d5a886688dd16b8c385901445b21a0a4c23ad0c5494aa89f36b6f5b839007b`
## 范围
仅完成 L0 和 L1;未进入 L2、CI/CD 或 Kubernetes,也未修改 gateway 透传、Secret、API Key 或上游请求语义。
@@ -233,6 +233,10 @@
为 SuperAPI 增加 skill description 管理面:先完成 L0,再完成 L1,不进入 L2;使用 native PostgreSQL 持久化 skill、脱敏 API Key 注册及别名、按 Key 绑定与启用开关;提供请求历史筛选及脱敏 request/response 详情;后端按 Temporal 规范拆分 API 与 workerSuperAPI gateway 保持独立进程;Web 使用左侧根导航和多页面工作区,并通过同一 superapi.hwpod.com L1 入口验收,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.7.13_Task_Report.md)。
#### R2.7.14 [completed]
改进 SuperAPI 请求历史详情可读性与 Skill 绑定弹窗布局:用 L0 CLI 验证 Request 的 system/user prompt、逐条 skill/tool,以及 Response 的 SSE 最终输出、错误和用量解析;L1 Web 通过同一个控件同步切换 Request/Response 的分块与 Raw 模式,每个 skill/tool 单行摘要并可展开详情;修复 Skill 下拉框显示不全,并从固定公网入口完成只读交互与截图验收,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.7.14_Task_Report.md)。
### R2.8 [completed]
整理本 MDTODO FILE 的职责层级和编号,并把同一 SUBITEM 下的直接 SUBSUBITEM 一般不超过 20 固化到 mdtodo-edit skill,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.8_Task_Report.md)。
@@ -174,7 +174,10 @@ SuperAPI 应提供左侧根导航和多页面管理工作区:
- 按时间、API Key、路径、状态和 request id 筛选;
- 默认分页展示摘要;
- 通过稳定 id 打开详情;
- 详情分别显示脱敏后的 request 与 response JSON。
- 详情通过同一个模式控件同步切换 request 与 response
- Raw 模式分别显示脱敏后的原始 request 与 response
- 分块模式解析 system prompt、user prompt、skill、tool、最终响应、错误和用量;
- 每个 skill 与 tool 独占一行摘要,并可展开查看完整脱敏字段。
Web 不得实现第二套业务逻辑。管理 API、默认本地 CLI 和显式 `--over-api` CLI
必须复用相同 contracts、repository port、application dispatcher、DTO 和错误语义。
+89
View File
@@ -12,6 +12,7 @@ interface SmokePage {
path: string;
readySelector: string;
screenshotName: string;
interaction: "none" | "api-key-bindings" | "history-detail";
}
interface SmokeProfile {
@@ -142,6 +143,7 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
const network = [];
const consoleErrors = [];
const pages = [];
const interactions = [];
const screenshots = [];
let networkCount = 0;
let consoleCount = 0;
@@ -202,6 +204,85 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
}
const shot = await screenshot(item.screenshotName);
screenshots.push(shot);
if (item.interaction === "api-key-bindings" && dom.rowCount > 0) {
await page.locator(item.readySelector + " tbody tr").first().click();
const dialog = page.getByRole("dialog", { name: /Skill /u });
await dialog.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
const select = dialog.getByRole("combobox", { name: "选择要绑定的 Skill" });
const bindingEvidence = await select.evaluate((element) => {
const option = element.querySelector("option");
const rect = element.getBoundingClientRect();
return {
placeholder: option?.textContent?.trim() || null,
visible: rect.width > 0 && rect.height > 0,
width: Math.round(rect.width),
};
});
const ok = bindingEvidence.placeholder === "请选择要绑定的 Skill"
&& bindingEvidence.visible
&& bindingEvidence.width >= 240;
if (!ok) addFailure("API Key Skill 下拉框占位符或布局不完整");
const interactionShot = await screenshot("superapi-api-key-bindings.png");
screenshots.push(interactionShot);
interactions.push({ id: item.interaction, ok, ...bindingEvidence, screenshot: interactionShot });
await dialog.getByRole("button", { name: "关闭" }).click();
}
if (item.interaction === "history-detail" && dom.rowCount > 0) {
await page.locator(item.readySelector + " tbody tr").first().click();
const dialog = page.getByRole("dialog", { name: "请求详情" });
await dialog.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
const readablePanels = dialog.locator(".readable-columns > section");
const requestPanel = readablePanels.nth(0);
const requestSections = await readablePanels.nth(0).locator(".readable-section").evaluateAll((elements) =>
elements.map((element) => element.getAttribute("data-section-id")));
const responseSections = await readablePanels.nth(1).locator(".readable-section").evaluateAll((elements) =>
elements.map((element) => element.getAttribute("data-section-id")));
const readableVisible = await dialog.locator(".readable-columns").isVisible();
const skillNode = requestPanel.locator('[data-section-id="skills"] .readable-node').first();
const toolNode = requestPanel.locator('[data-section-id="tools"] .readable-node').first();
await skillNode.locator("summary").first().click();
await toolNode.locator("summary").first().click();
const skillDetailFields = await skillNode.locator(".readable-value").count();
const toolDetailFields = await toolNode.locator(".readable-value").count();
const semanticDetailsVisible = skillDetailFields >= 2 && toolDetailFields >= 2;
await dialog.getByRole("button", { name: "Raw", exact: true }).click();
const rawVisible = await dialog.locator(".raw-columns").isVisible();
const rawPanelCount = await dialog.locator(".raw-columns > section > pre").count();
const readableHiddenInRaw = await dialog.locator(".readable-columns").count() === 0;
await dialog.getByRole("button", { name: "分块", exact: true }).click();
const readableRestored = await dialog.locator(".readable-columns").isVisible();
const requestSemantic = ["systemPrompt", "userPrompts", "skills", "tools"].every((id) => requestSections.includes(id));
const responseSemantic = ["overview", "outputs", "usage"].every((id) => responseSections.includes(id));
const ok = readableVisible
&& requestSemantic
&& responseSemantic
&& semanticDetailsVisible
&& rawVisible
&& rawPanelCount === 2
&& readableHiddenInRaw
&& readableRestored;
if (!ok) addFailure("请求详情分块或 Raw 模式不可用");
const interactionShot = await screenshot("superapi-history-detail-readable.png");
screenshots.push(interactionShot);
interactions.push({
id: item.interaction,
ok,
requestSections,
responseSections,
requestSemantic,
responseSemantic,
skillDetailFields,
toolDetailFields,
semanticDetailsVisible,
readableVisible,
rawVisible,
rawPanelCount,
readableHiddenInRaw,
readableRestored,
screenshot: interactionShot,
});
await dialog.getByRole("button", { name: "关闭" }).click();
}
pages.push({ id: item.id, path: new URL(page.url()).pathname, dom, screenshot: shot });
recordStep("读取 SuperAPI " + item.id + " 页面", { ok: dom.pageId === item.id && !dom.error, path: new URL(page.url()).pathname, rowCount: dom.rowCount });
}
@@ -211,6 +292,7 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
profileName: config.profileName,
configRef: config.configRef,
pages,
interactions,
network: { count: networkCount, items: network, mutatingRequests },
console: { count: consoleCount, items: consoleErrors },
failures,
@@ -287,6 +369,7 @@ function smokeProfile(value: unknown, name: string): SmokeProfile {
path: pathValue(item.path, "page.path"),
readySelector: text(item.readySelector, "page.readySelector"),
screenshotName: text(item.screenshotName, "page.screenshotName"),
interaction: interaction(item.interaction),
};
}),
viewport: { width: integer(viewport.width, "viewport.width"), height: integer(viewport.height, "viewport.height") },
@@ -301,6 +384,12 @@ function smokeProfile(value: unknown, name: string): SmokeProfile {
};
}
function interaction(value: unknown): SmokePage["interaction"] {
if (value === undefined) return "none";
if (value === "api-key-bindings" || value === "history-detail") return value;
throw new Error("page.interaction 必须是 api-key-bindings 或 history-detail");
}
function record(value: unknown, name: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${name} 必须是对象`);
return value as Record<string, unknown>;