// 静态调试页通过配置 API 获取实际 Swagger JSON 地址,避免硬编码路由前缀。 const DEBUG_CONFIG_URL = "/api/debug/config"; const STORAGE_PREFIX = "flyshot.debug."; const HISTORY_LIMIT = 10; const STATIC_DOCS_ELEMENT_ID = "debug-static-docs"; const groupTitleByPrefix = [ // 基础与状态分组:探活和状态快照两个固定 API 路径 { match: function (op) { return op.path === "/healthz" || op.path === "/api/status/snapshot"; }, title: "基础与状态" }, // 默认兜底:剩余全部走 ControllerClient 兼容分组 { match: function () { return true; }, title: "ControllerClient 兼容" } ]; const state = { spec: null, operations: [], history: [], staticDocsByKey: loadStaticDocs() }; const requestBodySamples = { "POST /move_pose/": { x: 100.0, y: 200.0, z: 300.0, w: 0.0, p: 45.0, r: 0.0 } }; /** 简单的 escape:把任意字符串安全嵌入 textContent 之外的位置时使用。 */ function escapeHtml(value) { return String(value).replace(/[&<>"']/g, function (ch) { return { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[ch]; }); } /** 为 METHOD PATH 文档块构造统一索引键。 */ function buildOperationKey(method, path) { return String(method || "").toUpperCase() + " " + String(path || ""); } /** 读取 debug.html 中内嵌的静态说明 JSON,并建立 METHOD PATH 索引。 */ function loadStaticDocs() { const docNode = document.getElementById(STATIC_DOCS_ELEMENT_ID); if (!docNode) return {}; try { const parsed = JSON.parse(docNode.textContent || "{}"); const indexed = {}; Object.keys(parsed).forEach(function (key) { indexed[key] = parsed[key]; }); return indexed; } catch (error) { console.warn("读取静态调试说明失败:", error); return {}; } } /** 解析 OpenAPI 中的 $ref 引用,仅支持本地 components.schemas 形式。 */ function resolveRef(ref) { if (!ref || !state.spec) return null; const parts = ref.replace(/^#\//, "").split("/"); let cursor = state.spec; for (const part of parts) { if (cursor && Object.prototype.hasOwnProperty.call(cursor, part)) { cursor = cursor[part]; } else { return null; } } return cursor; } /** 展开 schema,统一处理 $ref / oneOf / anyOf / allOf。 */ function resolveSchema(schema, depth) { depth = depth || 0; if (!schema || depth > 4) return null; if (schema.$ref) { return resolveSchema(resolveRef(schema.$ref), depth + 1); } if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return resolveSchema(schema.oneOf[0], depth + 1); if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) return resolveSchema(schema.anyOf[0], depth + 1); if (Array.isArray(schema.allOf) && schema.allOf.length > 0) { const merged = { type: "object", properties: {}, required: [] }; schema.allOf.forEach(function (subSchema) { const resolved = resolveSchema(subSchema, depth + 1); if (!resolved) return; if (resolved.properties) Object.assign(merged.properties, resolved.properties); if (Array.isArray(resolved.required)) { resolved.required.forEach(function (name) { if (merged.required.indexOf(name) < 0) merged.required.push(name); }); } }); return merged; } return schema; } /** 根据 schema 生成默认 JSON 模板,用于自动填充请求体编辑器。 */ function buildSampleFromSchema(schema, depth) { depth = depth || 0; // 防御递归:复杂自引用 schema 在 4 层后停下,避免栈爆。 if (!schema || depth > 4) return null; const resolvedSchema = resolveSchema(schema, depth); if (!resolvedSchema) return null; const type = resolvedSchema.type || (resolvedSchema.properties ? "object" : "string"); switch (type) { case "object": { const result = {}; const props = resolvedSchema.properties || {}; Object.keys(props).forEach(function (key) { result[key] = buildSampleFromSchema(props[key], depth + 1); }); return result; } case "array": return []; case "integer": case "number": return 0; case "boolean": return false; case "string": default: if (resolvedSchema.enum && resolvedSchema.enum.length > 0) return resolvedSchema.enum[0]; return ""; } } /** 把 schema.type 翻译成 input type 与展示文本。 */ function inputKindForType(schema) { if (!schema) return { kind: "text", label: "string" }; const resolvedSchema = resolveSchema(schema, 0) || schema; const type = resolvedSchema.type || "string"; if (type === "boolean") return { kind: "checkbox", label: "boolean" }; if (type === "integer" || type === "number") return { kind: "number", label: type }; return { kind: "text", label: type }; } /** 把 schema 转成适合字段表展示的简短类型文本。 */ function describeSchemaType(schema) { const resolvedSchema = resolveSchema(schema, 0); if (!resolvedSchema) return "unknown"; if (resolvedSchema.type === "array") { const itemType = describeSchemaType(resolvedSchema.items); return itemType === "unknown" ? "array" : "array<" + itemType + ">"; } if (resolvedSchema.type) return resolvedSchema.type; if (resolvedSchema.properties) return "object"; return "unknown"; } /** 当静态说明缺失时,从普通 object schema 推导字段表。 */ function buildFieldDocsFromSchema(schema) { const resolvedSchema = resolveSchema(schema, 0); if (!resolvedSchema) return []; const schemaType = resolvedSchema.type || (resolvedSchema.properties ? "object" : ""); if (schemaType !== "object" || !resolvedSchema.properties) return []; const requiredSet = new Set(resolvedSchema.required || []); return Object.keys(resolvedSchema.properties).map(function (name) { const propertySchema = resolvedSchema.properties[name]; return { name: name, type: describeSchemaType(propertySchema), required: requiredSet.has(name), description: propertySchema && propertySchema.description ? propertySchema.description : "暂无说明" }; }); } /** 规范化静态字段说明,避免缺字段时前端渲染异常。 */ function normalizeFieldDocs(fieldDocs) { if (!Array.isArray(fieldDocs)) return []; return fieldDocs.map(function (fieldDoc) { return { name: fieldDoc && fieldDoc.name ? String(fieldDoc.name) : "", type: fieldDoc && fieldDoc.type ? String(fieldDoc.type) : "unknown", required: Boolean(fieldDoc && fieldDoc.required), description: fieldDoc && fieldDoc.description ? String(fieldDoc.description) : "暂无说明" }; }).filter(function (fieldDoc) { return fieldDoc.name !== ""; }); } /** 把 OpenAPI 的 paths 节点展开成扁平的 operation 列表,并合并静态说明。 */ function extractOperations(spec) { const operations = []; const paths = spec.paths || {}; Object.keys(paths).forEach(function (path) { const pathItem = paths[path] || {}; ["get", "post", "put", "delete", "patch", "options", "head"].forEach(function (method) { const op = pathItem[method]; if (!op) return; const parameters = (op.parameters || []).filter(function (p) { return p.in === "query" || p.in === "path"; }); let bodySchema = null; if (op.requestBody && op.requestBody.content) { const json = op.requestBody.content["application/json"]; if (json && json.schema) bodySchema = json.schema; } const operationKey = buildOperationKey(method, path); const staticDoc = state.staticDocsByKey[operationKey] || null; const staticFieldDocs = staticDoc ? normalizeFieldDocs(staticDoc.requestBodyFields) : []; const schemaFieldDocs = staticFieldDocs.length === 0 ? buildFieldDocsFromSchema(bodySchema) : []; operations.push({ method: method.toUpperCase(), path: path, summary: op.summary || "", description: op.description || "", tags: op.tags || [], parameters: parameters, bodySchema: bodySchema, staticDoc: staticDoc, mergedSummary: staticDoc && staticDoc.summary ? staticDoc.summary : "暂无说明", mergedDescription: staticDoc && staticDoc.description ? staticDoc.description : "暂无说明", requestBodyFieldDocs: staticFieldDocs.length > 0 ? staticFieldDocs : schemaFieldDocs }); }); }); return operations; } /** 选择分组:优先用第一条匹配的 groupTitleByPrefix 规则,OpenAPI tag 留作兜底。 */ function pickGroup(op) { for (const rule of groupTitleByPrefix) { if (rule.match(op)) return rule.title; } if (op.tags && op.tags.length > 0) return op.tags[0]; return "其它"; } /** localStorage key 必须避免冲突,使用 method:path 复合键。 */ function storageKey(op) { return STORAGE_PREFIX + op.method + ":" + op.path; } /** 返回指定端点的手工调试样例,处理 JsonElement 等 OpenAPI 无法自动推断字段的接口。 */ function getRequestBodySample(op) { const operationKey = op.method + " " + op.path; return requestBodySamples[operationKey] || null; } /** 读取本端点最近一次输入;解析失败则当作空。 */ function loadInputs(op) { try { const raw = window.localStorage.getItem(storageKey(op)); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } /** 保存本端点最近一次输入;写入失败时静默忽略,避免影响调试体验。 */ function saveInputs(op, payload) { try { window.localStorage.setItem(storageKey(op), JSON.stringify(payload)); } catch (e) { // localStorage 可能被禁用或满载,忽略写入失败。 } } /** 拼接最终请求 URL(含 query 串与 path 参数替换)。 */ function buildRequestUrl(op, paramValues) { let path = op.path; const queryPairs = []; op.parameters.forEach(function (param) { const raw = paramValues[param.name]; if (raw === undefined || raw === null || raw === "") return; if (param.in === "path") { path = path.replace("{" + param.name + "}", encodeURIComponent(raw)); } else if (param.in === "query") { queryPairs.push(encodeURIComponent(param.name) + "=" + encodeURIComponent(raw)); } }); return path + (queryPairs.length > 0 ? "?" + queryPairs.join("&") : ""); } /** 生成与浏览器请求等价的 curl 命令,便于复制到终端复现。 */ function buildCurlCommand(op, requestUrl, body) { const parts = ["curl", "-X", op.method, JSON.stringify(window.location.origin + requestUrl)]; if (body !== null && body !== undefined && body !== "") { parts.push("-H", "\"Content-Type: application/json\""); parts.push("--data-raw", JSON.stringify(body)); } return parts.join(" "); } /** 渲染参数输入表单,返回收集函数。 */ function renderParameterRows(container, op, savedValues) { if (op.parameters.length === 0) return function () { return {}; }; const inputs = {}; op.parameters.forEach(function (param) { const row = document.createElement("div"); row.className = "form-row"; const nameNode = document.createElement("div"); nameNode.className = "name"; nameNode.textContent = param.name + " (" + param.in + ")"; if (param.required) { const requiredMark = document.createElement("span"); requiredMark.className = "required"; requiredMark.textContent = "*"; nameNode.appendChild(requiredMark); } row.appendChild(nameNode); const kind = inputKindForType(param.schema); const inputNode = document.createElement("input"); inputNode.type = kind.kind; if (kind.kind === "checkbox") { inputNode.checked = savedValues && Object.prototype.hasOwnProperty.call(savedValues, param.name) ? Boolean(savedValues[param.name]) : Boolean(param.schema && param.schema.default); } else { let initial = ""; if (savedValues && Object.prototype.hasOwnProperty.call(savedValues, param.name)) { initial = String(savedValues[param.name]); } else if (param.schema && param.schema.default !== undefined) { initial = String(param.schema.default); } inputNode.value = initial; if (kind.kind === "number") inputNode.step = "any"; } row.appendChild(inputNode); const typeNode = document.createElement("div"); typeNode.className = "type"; typeNode.textContent = kind.label; row.appendChild(typeNode); container.appendChild(row); inputs[param.name] = { node: inputNode, kind: kind.kind, schema: param.schema }; }); return function collect() { const collected = {}; Object.keys(inputs).forEach(function (key) { const item = inputs[key]; if (item.kind === "checkbox") { collected[key] = item.node.checked; } else { const raw = item.node.value; if (raw === "") { collected[key] = ""; } else if (item.kind === "number") { const num = Number(raw); collected[key] = Number.isNaN(num) ? raw : num; } else { collected[key] = raw; } } }); return collected; }; } /** 渲染请求体字段说明表;静态说明缺失时退回 schema 推导。 */ function renderRequestBodyFieldDocs(container, op) { if (!op.bodySchema && op.requestBodyFieldDocs.length === 0) return; const wrapper = document.createElement("div"); wrapper.className = "body-block"; const title = document.createElement("div"); title.className = "body-label"; const left = document.createElement("div"); left.className = "left"; left.textContent = "请求体字段说明"; title.appendChild(left); wrapper.appendChild(title); if (op.requestBodyFieldDocs.length === 0) { const empty = document.createElement("div"); empty.style.color = "var(--muted)"; empty.style.fontSize = "13px"; empty.textContent = "暂无说明"; wrapper.appendChild(empty); container.appendChild(wrapper); return; } const table = document.createElement("table"); table.style.width = "100%"; table.style.borderCollapse = "collapse"; table.style.fontSize = "13px"; const headerRow = document.createElement("tr"); ["字段名", "类型", "必填", "说明"].forEach(function (text) { const th = document.createElement("th"); th.textContent = text; th.style.textAlign = "left"; th.style.padding = "6px 8px"; th.style.borderBottom = "1px solid rgba(148, 163, 184, 0.25)"; th.style.color = "var(--muted)"; headerRow.appendChild(th); }); table.appendChild(headerRow); op.requestBodyFieldDocs.forEach(function (fieldDoc) { const row = document.createElement("tr"); [ fieldDoc.name, fieldDoc.type, fieldDoc.required ? "是" : "否", fieldDoc.description || "暂无说明" ].forEach(function (value, index) { const cell = document.createElement("td"); cell.style.padding = "8px"; cell.style.verticalAlign = "top"; cell.style.borderBottom = "1px solid rgba(148, 163, 184, 0.12)"; if (index === 0 || index === 1) { cell.style.fontFamily = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; } cell.textContent = value; row.appendChild(cell); }); table.appendChild(row); }); wrapper.appendChild(table); container.appendChild(wrapper); } /** 渲染请求体编辑器,返回收集函数。 */ function renderBodyEditor(container, op, savedBody) { if (!op.bodySchema) return function () { return null; }; // 字段说明先于 JSON 编辑器出现,便于现场先看约定再改样例。 renderRequestBodyFieldDocs(container, op); const block = document.createElement("div"); block.className = "body-block"; const labelRow = document.createElement("div"); labelRow.className = "body-label"; const left = document.createElement("div"); left.className = "left"; left.textContent = "请求体 (application/json)"; labelRow.appendChild(left); const formatBtn = document.createElement("button"); formatBtn.type = "button"; formatBtn.className = "secondary"; formatBtn.textContent = "格式化 JSON"; labelRow.appendChild(formatBtn); block.appendChild(labelRow); const textarea = document.createElement("textarea"); textarea.className = "body-editor"; textarea.spellcheck = false; let initialText; if (savedBody !== undefined && savedBody !== null) { initialText = typeof savedBody === "string" ? savedBody : JSON.stringify(savedBody, null, 2); } else { const sample = getRequestBodySample(op) || buildSampleFromSchema(op.bodySchema, 0); initialText = sample === null ? "" : JSON.stringify(sample, null, 2); } textarea.value = initialText; block.appendChild(textarea); formatBtn.addEventListener("click", function () { try { const parsed = JSON.parse(textarea.value || "null"); textarea.value = parsed === null ? "" : JSON.stringify(parsed, null, 2); } catch (e) { window.alert("JSON 解析失败: " + e.message); } }); container.appendChild(block); return function collect() { return textarea.value; }; } /** 把 HTTP 状态码翻译成颜色徽标 class。 */ function statusBadgeClass(status) { if (status >= 200 && status < 300) return "s2xx"; if (status >= 300 && status < 400) return "s3xx"; if (status >= 400 && status < 500) return "s4xx"; if (status >= 500) return "s5xx"; return "error"; } /** 把响应头展开成可读字符串。 */ function formatHeaders(headers) { const lines = []; headers.forEach(function (value, key) { lines.push(key + ": " + value); }); return lines.join("\n"); } /** 在历史面板顶部追加一条记录,超过上限则丢弃尾部。 */ function pushHistory(entry) { state.history.unshift(entry); if (state.history.length > HISTORY_LIMIT) state.history.length = HISTORY_LIMIT; renderHistory(); } /** 渲染请求历史列表。 */ function renderHistory() { const list = document.getElementById("history-list"); list.innerHTML = ""; if (state.history.length === 0) { const empty = document.createElement("li"); empty.textContent = "暂无调用记录"; empty.style.color = "var(--muted)"; empty.style.gridTemplateColumns = "1fr"; list.appendChild(empty); return; } state.history.forEach(function (entry) { const li = document.createElement("li"); const method = document.createElement("span"); method.className = "h-method"; method.textContent = entry.method; method.style.color = entry.method === "GET" ? "var(--get)" : entry.method === "POST" ? "var(--post)" : "var(--muted)"; const path = document.createElement("span"); path.className = "h-path"; path.title = entry.url; path.textContent = entry.url; const meta = document.createElement("span"); meta.style.color = "var(--muted)"; meta.textContent = (entry.status || "ERR") + " · " + entry.elapsedMs + "ms"; li.appendChild(method); li.appendChild(path); li.appendChild(meta); list.appendChild(li); }); } /** 渲染单个端点的卡片。 */ function renderOperationCard(op) { const card = document.createElement("section"); card.className = "card"; const head = document.createElement("div"); head.className = "card-head"; const badge = document.createElement("span"); badge.className = "badge " + (["GET", "POST", "PUT", "DELETE"].indexOf(op.method) >= 0 ? op.method : "OTHER"); badge.textContent = op.method; head.appendChild(badge); const path = document.createElement("span"); path.className = "card-path"; path.textContent = op.path; head.appendChild(path); const summary = document.createElement("span"); summary.className = "card-summary"; summary.textContent = op.mergedSummary; summary.title = op.mergedSummary; head.appendChild(summary); const toggle = document.createElement("span"); toggle.className = "card-toggle"; toggle.textContent = "收起 ▴"; head.appendChild(toggle); head.addEventListener("click", function () { const collapsed = card.classList.toggle("collapsed"); toggle.textContent = collapsed ? "展开 ▾" : "收起 ▴"; }); card.appendChild(head); const body = document.createElement("div"); body.className = "card-body"; const desc = document.createElement("div"); desc.style.color = "var(--muted)"; desc.style.marginBottom = "10px"; desc.style.fontSize = "13px"; desc.textContent = op.mergedDescription; body.appendChild(desc); const saved = loadInputs(op) || {}; // 参数区 let collectParams = function () { return {}; }; if (op.parameters.length > 0) { const paramsContainer = document.createElement("div"); paramsContainer.className = "params"; body.appendChild(paramsContainer); collectParams = renderParameterRows(paramsContainer, op, saved.params); } // 请求体区 const collectBody = renderBodyEditor(body, op, saved.body); // 操作按钮 const buttonRow = document.createElement("div"); buttonRow.className = "button-row"; const sendBtn = document.createElement("button"); sendBtn.type = "button"; sendBtn.textContent = "发送"; const resetBtn = document.createElement("button"); resetBtn.type = "button"; resetBtn.className = "secondary"; resetBtn.textContent = "重置"; const curlBtn = document.createElement("button"); curlBtn.type = "button"; curlBtn.className = "secondary"; curlBtn.textContent = "复制 curl"; buttonRow.appendChild(sendBtn); buttonRow.appendChild(resetBtn); buttonRow.appendChild(curlBtn); body.appendChild(buttonRow); // 响应面板 const responseBlock = document.createElement("div"); responseBlock.className = "response-block"; responseBlock.style.display = "none"; body.appendChild(responseBlock); /** 在卡片内渲染 HTTP 响应摘要、头和正文。 */ function renderResponse(payload) { responseBlock.style.display = "block"; responseBlock.innerHTML = ""; const summaryRow = document.createElement("div"); summaryRow.className = "response-summary"; const statusBadge = document.createElement("span"); statusBadge.className = "status-badge " + statusBadgeClass(payload.status || 0); statusBadge.textContent = payload.status ? payload.status + " " + (payload.statusText || "") : "请求失败"; summaryRow.appendChild(statusBadge); const elapsed = document.createElement("span"); elapsed.style.color = "var(--muted)"; elapsed.textContent = payload.elapsedMs + " ms · " + payload.url; summaryRow.appendChild(elapsed); responseBlock.appendChild(summaryRow); if (payload.error) { const pre = document.createElement("pre"); pre.className = "response-body"; pre.textContent = payload.error; responseBlock.appendChild(pre); return; } const headersDetails = document.createElement("details"); const headersSummary = document.createElement("summary"); headersSummary.textContent = "响应头"; headersDetails.appendChild(headersSummary); const headersPre = document.createElement("pre"); headersPre.className = "response-headers"; headersPre.textContent = payload.headers; headersDetails.appendChild(headersPre); responseBlock.appendChild(headersDetails); const bodyPre = document.createElement("pre"); bodyPre.className = "response-body"; bodyPre.textContent = payload.bodyText; responseBlock.appendChild(bodyPre); } sendBtn.addEventListener("click", async function () { sendBtn.disabled = true; const params = collectParams(); const rawBody = collectBody(); saveInputs(op, { params: params, body: rawBody }); const requestUrl = buildRequestUrl(op, params); const init = { method: op.method, headers: {} }; // 仅 POST/PUT/PATCH/DELETE 才认为可能携带 body;对没有 bodySchema 的方法直接跳过。 const methodAllowsBody = ["POST", "PUT", "PATCH", "DELETE"].indexOf(op.method) >= 0; if (methodAllowsBody && op.bodySchema && rawBody !== null && rawBody !== undefined && rawBody !== "") { init.headers["Content-Type"] = "application/json"; init.body = rawBody; } const startedAt = performance.now(); try { const response = await fetch(requestUrl, init); const elapsedMs = Math.round(performance.now() - startedAt); const text = await response.text(); const contentType = response.headers.get("content-type") || ""; let bodyText = text; if (contentType.indexOf("application/json") >= 0) { try { bodyText = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { bodyText = text; } } renderResponse({ status: response.status, statusText: response.statusText, headers: formatHeaders(response.headers), bodyText: bodyText, url: requestUrl, elapsedMs: elapsedMs }); pushHistory({ method: op.method, url: requestUrl, status: response.status, elapsedMs: elapsedMs }); } catch (err) { const elapsedMs = Math.round(performance.now() - startedAt); renderResponse({ error: String(err && err.message ? err.message : err), url: requestUrl, elapsedMs: elapsedMs }); pushHistory({ method: op.method, url: requestUrl, status: 0, elapsedMs: elapsedMs }); } finally { sendBtn.disabled = false; } }); resetBtn.addEventListener("click", function () { try { window.localStorage.removeItem(storageKey(op)); } catch (e) { /* 忽略 */ } // 直接重新渲染当前卡片:替换原 DOM 节点。 const refreshed = renderOperationCard(op); refreshed.classList.remove("collapsed"); refreshed.querySelector(".card-toggle").textContent = "收起 ▴"; card.parentNode.replaceChild(refreshed, card); }); curlBtn.addEventListener("click", function () { const params = collectParams(); const rawBody = collectBody(); const requestUrl = buildRequestUrl(op, params); const methodAllowsBody = ["POST", "PUT", "PATCH", "DELETE"].indexOf(op.method) >= 0; const bodyForCurl = methodAllowsBody && op.bodySchema ? rawBody : null; const command = buildCurlCommand(op, requestUrl, bodyForCurl); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(command).then(function () { curlBtn.textContent = "已复制 ✓"; window.setTimeout(function () { curlBtn.textContent = "复制 curl"; }, 1500); }).catch(function () { window.prompt("复制失败,手动复制:", command); }); } else { window.prompt("复制失败,手动复制:", command); } }); card.appendChild(body); return card; } /** 把 operation 列表按分组渲染到主区域。 */ function renderGroups(operations) { const root = document.getElementById("debug-console-app"); root.innerHTML = ""; if (operations.length === 0) { const empty = document.createElement("div"); empty.className = "empty-hint"; empty.textContent = "OpenAPI 文档中没有任何端点。"; root.appendChild(empty); return; } const grouped = new Map(); operations.forEach(function (op) { const groupTitle = pickGroup(op); if (!grouped.has(groupTitle)) grouped.set(groupTitle, []); grouped.get(groupTitle).push(op); }); // 固定输出顺序:基础与状态在前,ControllerClient 兼容在后,其余按字典序。 const orderedTitles = []; ["基础与状态", "ControllerClient 兼容"].forEach(function (title) { if (grouped.has(title)) orderedTitles.push(title); }); Array.from(grouped.keys()).sort().forEach(function (title) { if (orderedTitles.indexOf(title) < 0) orderedTitles.push(title); }); orderedTitles.forEach(function (title) { const ops = grouped.get(title); ops.sort(function (a, b) { if (a.path === b.path) return a.method.localeCompare(b.method); return a.path.localeCompare(b.path); }); const section = document.createElement("section"); section.className = "group"; const heading = document.createElement("h2"); heading.textContent = title + " (" + ops.length + ")"; section.appendChild(heading); ops.forEach(function (op) { section.appendChild(renderOperationCard(op)); }); root.appendChild(section); }); } /** 加载 OpenAPI 文档并渲染。 */ async function loadSpecAndRender() { const metaSpec = document.getElementById("meta-spec-url"); const metaCount = document.getElementById("meta-operation-count"); const metaStatus = document.getElementById("meta-status"); metaSpec.textContent = "正在读取调试配置..."; metaStatus.textContent = "正在拉取 OpenAPI 文档..."; metaStatus.className = ""; try { const configResponse = await fetch(DEBUG_CONFIG_URL, { cache: "no-store" }); if (!configResponse.ok) throw new Error("调试配置 HTTP " + configResponse.status + " " + configResponse.statusText); const config = await configResponse.json(); const swaggerJsonUrl = config.swaggerJsonUrl; if (!swaggerJsonUrl) throw new Error("调试配置缺少 swaggerJsonUrl"); metaSpec.textContent = swaggerJsonUrl; const response = await fetch(swaggerJsonUrl, { cache: "no-store" }); if (!response.ok) throw new Error("HTTP " + response.status + " " + response.statusText); const spec = await response.json(); state.spec = spec; state.operations = extractOperations(spec); metaCount.textContent = state.operations.length; metaStatus.textContent = "已加载"; metaStatus.className = "good"; renderGroups(state.operations); } catch (err) { metaStatus.textContent = "加载失败: " + (err && err.message ? err.message : err); metaStatus.className = "bad"; metaCount.textContent = "0"; const root = document.getElementById("debug-console-app"); root.innerHTML = ""; const errBlock = document.createElement("div"); errBlock.className = "empty-hint"; errBlock.textContent = "无法加载 OpenAPI 文档,请确认 Swagger:Enabled = true 且 " + DEBUG_CONFIG_URL + " 可访问。"; root.appendChild(errBlock); } } document.getElementById("reload-spec").addEventListener("click", loadSpecAndRender); document.getElementById("history-clear").addEventListener("click", function () { state.history.length = 0; renderHistory(); }); renderHistory(); loadSpecAndRender();