diff --git a/docs/MDTODO/details/sub2api-upstream-reliability/R2.7.14_Task_Report.md b/docs/MDTODO/details/sub2api-upstream-reliability/R2.7.14_Task_Report.md new file mode 100644 index 00000000..314b3014 --- /dev/null +++ b/docs/MDTODO/details/sub2api-upstream-reliability/R2.7.14_Task_Report.md @@ -0,0 +1,33 @@ +# R2.7.14 任务报告 + +## 交付结果 + +- SuperAPI 请求详情增加同一个 `分块 / Raw` 模式控件,Request 与 Response 始终同步切换。 +- Request 分块解析请求概要、System Prompt、Developer Prompt、User Prompt、Skills 和 Tools;SuperAPI 注入的 `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 或上游请求语义。 diff --git a/docs/MDTODO/sub2api-upstream-reliability.md b/docs/MDTODO/sub2api-upstream-reliability.md index 98c40feb..8e1c84c3 100644 --- a/docs/MDTODO/sub2api-upstream-reliability.md +++ b/docs/MDTODO/sub2api-upstream-reliability.md @@ -233,6 +233,10 @@ 为 SuperAPI 增加 skill description 管理面:先完成 L0,再完成 L1,不进入 L2;使用 native PostgreSQL 持久化 skill、脱敏 API Key 注册及别名、按 Key 绑定与启用开关;提供请求历史筛选及脱敏 request/response 详情;后端按 Temporal 规范拆分 API 与 worker,SuperAPI 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)。 diff --git a/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md b/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md index 089960ad..72371257 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.md +++ b/project-management/PJ2026-01/specs/PJ2026-01050205-superapi.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 和错误语义。 diff --git a/scripts/src/web-probe-superapi-smoke.ts b/scripts/src/web-probe-superapi-smoke.ts index b99a1139..0552fd78 100644 --- a/scripts/src/web-probe-superapi-smoke.ts +++ b/scripts/src/web-probe-superapi-smoke.ts @@ -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 { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${name} 必须是对象`); return value as Record;