render.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. // 把 API 协议参考渲染为独立的 HTML 文档(不引用任何外部资源):后台页面内嵌显示,也用于生成 PDF
  2. import { API_ERRORS, API_EVENTS, API_GROUPS, API_OVERVIEW, API_TYPES, type Endpoint, type Field } from './reference'
  3. export interface RenderOptions {
  4. /** 文档中示例使用的服务地址 */
  5. baseUrl: string
  6. theme: 'light' | 'dark'
  7. version: string
  8. generatedAt: Date
  9. }
  10. const esc = (s: string) => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!))
  11. /** 行内代码:`xxx` → <code> */
  12. const inline = (s: string) => esc(s).replace(/`([^`]+)`/g, '<code>$1</code>')
  13. /** 概述条目:`代码`、**加粗**,$BASE 替换为服务地址 */
  14. const marked = (s: string, base: string) => inline(s.replaceAll('$BASE', base)).replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
  15. /** "409 session_busy:说明" → 状态码加粗、错误码为代码样式 */
  16. const errorItem = (x: string) => {
  17. const m = /^(\d{3}) ([a-z_]+)(.*)$/.exec(x)
  18. return m ? `<li><b>${m[1]}</b> <code>${m[2]}</code>${inline(m[3]!)}</li>` : `<li>${inline(x)}</li>`
  19. }
  20. const anchor = (e: Endpoint) => `${e.method}-${e.path}`.toLowerCase().replace(/[^a-z0-9]+/g, '-')
  21. function fieldTable(fields: Field[], withRequired = true) {
  22. return `<table class="fields"><thead><tr><th>字段</th><th>类型</th>${withRequired ? '<th>必填</th>' : ''}<th>说明</th></tr></thead><tbody>${
  23. fields.map(f => `<tr><td><code>${esc(f.name)}</code></td><td><code class="type">${esc(f.type)}</code></td>${withRequired ? `<td>${f.required ? '是' : ''}</td>` : ''}<td>${inline(f.desc)}</td></tr>`).join('')
  24. }</tbody></table>`
  25. }
  26. function endpoint(e: Endpoint, base: string) {
  27. return `<section class="endpoint" id="${anchor(e)}">
  28. <h3><span class="method ${e.method.toLowerCase()}">${e.method}</span><code class="path">${esc(e.path)}</code><span class="summary">${esc(e.summary)}</span></h3>
  29. ${e.desc ? `<p>${inline(e.desc)}</p>` : ''}
  30. ${e.query?.length ? `<h4>查询参数</h4>${fieldTable(e.query)}` : ''}
  31. ${e.body?.length ? `<h4>请求体(JSON)</h4>${fieldTable(e.body)}` : ''}
  32. ${e.bodyNote ? `<h4>请求体</h4><p>${inline(e.bodyNote)}</p>` : ''}
  33. <h4>响应</h4><p><span class="status">${e.response.status}</span> <code class="type">${esc(e.response.type)}</code>${e.response.desc ? ` — ${inline(e.response.desc)}` : ''}</p>
  34. ${e.errors?.length ? `<h4>常见错误</h4><ul>${e.errors.map(errorItem).join('')}</ul>` : ''}
  35. ${e.example ? `<h4>示例</h4><pre>${esc(e.example.replaceAll('$BASE', base))}</pre>` : ''}
  36. </section>`
  37. }
  38. export function renderApiReferenceHtml(o: RenderOptions): string {
  39. const base = o.baseUrl.replace(/\/+$/, '')
  40. const date = o.generatedAt.toISOString().slice(0, 10)
  41. const toc = [
  42. ['overview', '1. 概述'],
  43. ...API_GROUPS.map((g, i) => [g.id, `${i + 2}. ${g.title}`]),
  44. ['event-types', `${API_GROUPS.length + 2}. 事件类型`],
  45. ['types', `${API_GROUPS.length + 3}. 数据结构`],
  46. ['errors', `${API_GROUPS.length + 4}. 错误码`],
  47. ]
  48. return `<!doctype html>
  49. <html lang="zh-CN" data-theme="${o.theme}">
  50. <head>
  51. <meta charset="utf-8">
  52. <meta name="viewport" content="width=device-width, initial-scale=1">
  53. <title>AgentServer API 协议 ${esc(o.version)}</title>
  54. <style>
  55. :root { --bg:#ffffff; --fg:#1f2328; --muted:#59636e; --border:#d1d9e0; --code-bg:#f6f8fa; --link:#0969da;
  56. --get:#1a7f37; --post:#0969da; --patch:#9a6700; --delete:#cf222e; }
  57. [data-theme="dark"] { --bg:#1f1f1f; --fg:#cccccc; --muted:#9d9d9d; --border:#3c3c3c; --code-bg:#2b2b2b; --link:#4daafc;
  58. --get:#3fb950; --post:#58a6ff; --patch:#d29922; --delete:#f85149; }
  59. * { box-sizing: border-box; }
  60. html, body { margin: 0; background: var(--bg); color: var(--fg); }
  61. body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif;
  62. font-size: 13.5px; line-height: 1.65; padding: 32px 40px 64px; max-width: 980px; margin: 0 auto; }
  63. h1 { font-size: 26px; margin: 0 0 4px; }
  64. h2 { font-size: 19px; margin: 36px 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
  65. h3 { font-size: 15px; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  66. h4 { font-size: 12.5px; margin: 14px 0 6px; color: var(--muted); }
  67. p, ul { margin: 6px 0; }
  68. a { color: var(--link); text-decoration: none; }
  69. code, pre { font-family: "JetBrains Mono", "Cascadia Code", Menlo, Consolas, "Noto Sans Mono CJK SC", monospace; font-size: 12px; }
  70. code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }
  71. code.type { color: var(--muted); }
  72. pre { background: var(--code-bg); padding: 10px 12px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; margin: 6px 0; }
  73. .meta { color: var(--muted); margin-bottom: 20px; }
  74. .toc { columns: 2; padding-left: 18px; margin: 8px 0 0; }
  75. table { border-collapse: collapse; width: 100%; margin: 6px 0; }
  76. th, td { border: 1px solid var(--border); padding: 5px 8px; text-align: left; vertical-align: top; }
  77. th { background: var(--code-bg); font-weight: 600; white-space: nowrap; }
  78. td code { white-space: nowrap; }
  79. td code.type { white-space: normal; }
  80. .endpoint { border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin: 14px 0; }
  81. .method { font: 700 11.5px/1 sans-serif; color: #fff; padding: 4px 7px; border-radius: 4px; }
  82. .method.get { background: var(--get); } .method.post { background: var(--post); } .method.patch { background: var(--patch); } .method.delete { background: var(--delete); }
  83. code.path { font-size: 13px; background: none; padding: 0; font-weight: 600; }
  84. .summary { color: var(--muted); font-weight: 500; }
  85. .status { font-weight: 700; }
  86. .group-desc { color: var(--muted); }
  87. .no { color: var(--muted); }
  88. @page { size: A4; margin: 14mm 12mm; }
  89. @media print {
  90. :root, [data-theme="dark"] { --bg:#ffffff; --fg:#1f2328; --muted:#59636e; --border:#d1d9e0; --code-bg:#f6f8fa; --link:#0969da;
  91. --get:#1a7f37; --post:#0969da; --patch:#9a6700; --delete:#cf222e; }
  92. body { padding: 0; max-width: none; font-size: 11.5px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  93. .endpoint, table, pre { break-inside: avoid; }
  94. h2 { break-after: avoid; }
  95. }
  96. </style>
  97. </head>
  98. <body>
  99. <h1>AgentServer 对外 API 协议</h1>
  100. <div class="meta">版本 ${esc(o.version)} · 服务地址 <code>${esc(base)}</code> · 生成于 ${date}</div>
  101. <h2>目录</h2>
  102. <ol class="toc">${toc.map(([id, t]) => `<li><a href="#${id}">${esc(t!.replace(/^\d+\. /, ''))}</a></li>`).join('')}</ol>
  103. <h2 id="overview">1. 概述</h2>
  104. <ul>${API_OVERVIEW.points.map(x => `<li>${marked(x, base)}</li>`).join('')}</ul>
  105. <h4>典型流程</h4>
  106. <ol>${API_OVERVIEW.flow.map(x => `<li>${marked(x, base)}</li>`).join('')}</ol>
  107. ${API_GROUPS.map((g, i) => `<h2 id="${g.id}">${i + 2}. ${esc(g.title)}</h2>
  108. ${g.desc ? `<p class="group-desc">${inline(g.desc)}</p>` : ''}
  109. ${g.endpoints.map(e => endpoint(e, base)).join('\n')}`).join('\n')}
  110. <h2 id="event-types">${API_GROUPS.length + 2}. 事件类型</h2>
  111. <p>事件信封见"数据结构 › AgentEvent"。"落库"为否的事件只在实时事件流中推送,历史事件中只保留对应的 completed 事件。</p>
  112. <table><thead><tr><th>type</th><th>payload</th><th>说明</th><th>落库</th></tr></thead><tbody>
  113. ${API_EVENTS.map(e => `<tr><td><code>${esc(e.type)}</code></td><td><code class="type">${esc(e.payload)}</code></td><td>${inline(e.desc)}</td><td>${e.persisted ? '是' : '<span class="no">否</span>'}</td></tr>`).join('\n')}
  114. </tbody></table>
  115. <h2 id="types">${API_GROUPS.length + 3}. 数据结构</h2>
  116. ${API_TYPES.map(t => `<h3 id="type-${t.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}"><code class="path">${esc(t.name)}</code></h3>
  117. ${t.desc ? `<p class="group-desc">${inline(t.desc)}</p>` : ''}${fieldTable(t.fields, false)}`).join('\n')}
  118. <h2 id="errors">${API_GROUPS.length + 4}. 错误码</h2>
  119. <table><thead><tr><th>HTTP</th><th>code</th><th>说明</th></tr></thead><tbody>
  120. ${API_ERRORS.map(e => `<tr><td>${e.status}</td><td><code>${esc(e.code)}</code></td><td>${inline(e.desc)}</td></tr>`).join('\n')}
  121. </tbody></table>
  122. </body>
  123. </html>`
  124. }