// 把 API 协议参考渲染为独立的 HTML 文档(不引用任何外部资源):新窗口打开与生成 PDF。两套协议(Agent API、Bot API)各为一部分 import type { ApiSpec } from '@agent-server/shared' import { API_SPECS, type Endpoint, type Field } from './reference' export interface RenderOptions { /** 文档中示例使用的服务地址 */ baseUrl: string theme: 'light' | 'dark' version: string generatedAt: Date } const esc = (s: string) => s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!)) /** 行内代码:`xxx` → */ const inline = (s: string) => esc(s).replace(/`([^`]+)`/g, '$1') /** 概述条目:`代码`、**加粗**,$BASE 替换为服务地址 */ const marked = (s: string, base: string) => inline(s.replaceAll('$BASE', base)).replace(/\*\*([^*]+)\*\*/g, '$1') /** "409 session_busy:说明" → 状态码加粗、错误码为代码样式 */ const errorItem = (x: string) => { const m = /^(\d{3}) ([a-z_]+)(.*)$/.exec(x) return m ? `
  • ${m[1]} ${m[2]}${inline(m[3]!)}
  • ` : `
  • ${inline(x)}
  • ` } const anchor = (e: Endpoint) => `${e.method}-${e.path}`.toLowerCase().replace(/[^a-z0-9]+/g, '-') const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-') function fieldTable(fields: Field[], withRequired = true) { return `${withRequired ? '' : ''}${ fields.map(f => `${withRequired ? `` : ''}`).join('') }
    字段类型必填说明
    ${esc(f.name)}${esc(f.type)}${f.required ? '是' : ''}${inline(f.desc)}
    ` } function endpoint(e: Endpoint, base: string) { return `

    ${e.method}${esc(e.path)}${esc(e.summary)}

    ${e.desc ? `

    ${inline(e.desc)}

    ` : ''} ${e.query?.length ? `

    查询参数

    ${fieldTable(e.query)}` : ''} ${e.body?.length ? `

    请求体(JSON)

    ${fieldTable(e.body)}` : ''} ${e.bodyNote ? `

    请求体

    ${inline(e.bodyNote)}

    ` : ''}

    响应

    ${e.response.status} ${esc(e.response.type)}${e.response.desc ? ` — ${inline(e.response.desc)}` : ''}

    ${e.errors?.length ? `

    常见错误

    ` : ''} ${e.example ? `

    示例

    ${esc(e.example.replaceAll('$BASE', base))}
    ` : ''}
    ` } /** 一部分的章节:概述、各接口分组、事件类型、数据结构、错误码(编号为"部分.节") */ function sections(spec: ApiSpec) { return [ ['overview', '概述'], ...spec.groups.map(g => [g.id, g.title]), ...(spec.events.length ? [['event-types', '事件类型']] : []), ['types', '数据结构'], ['errors', '错误码'], ].map(([id, title], k) => ({ id: `${spec.id}-${id}`, title: title!, no: k + 1 })) } function specHtml(spec: ApiSpec, part: number, base: string) { const sec = sections(spec) const no = (id: string) => `${part}.${sec.find(x => x.id === `${spec.id}-${id}`)!.no}` return `

    第 ${part} 部分 · ${esc(spec.title)}

    ${inline(spec.summary)}

    ${no('overview')} 概述

    典型流程

      ${spec.overview.flow.map(x => `
    1. ${marked(x, base)}
    2. `).join('')}
    ${spec.groups.map(g => `

    ${no(g.id)} ${esc(g.title)}

    ${g.desc ? `

    ${inline(g.desc)}

    ` : ''} ${g.endpoints.map(e => endpoint(e, base)).join('\n')}`).join('\n')} ${spec.events.length ? `

    ${no('event-types')} 事件类型

    事件信封见"数据结构 › ${esc(spec.eventEnvelope)}"。"落库"为否的事件只在实时事件流中推送,不出现在历史事件中。

    ${spec.events.map(e => ``).join('\n')}
    typepayload说明落库
    ${esc(e.type)}${esc(e.payload)}${inline(e.desc)}${e.persisted ? '是' : '否'}
    ` : ''}

    ${no('types')} 数据结构

    ${spec.types.map(t => `

    ${esc(t.name)}

    ${t.desc ? `

    ${inline(t.desc)}

    ` : ''}${fieldTable(t.fields, false)}`).join('\n')}

    ${no('errors')} 错误码

    ${spec.errors.map(e => ``).join('\n')}
    HTTPcode说明
    ${e.status}${esc(e.code)}${inline(e.desc)}
    ` } export function renderApiReferenceHtml(o: RenderOptions): string { const base = o.baseUrl.replace(/\/+$/, '') const date = o.generatedAt.toISOString().slice(0, 10) return ` AgentServer API 协议 ${esc(o.version)}

    AgentServer 对外 API 协议

    版本 ${esc(o.version)} · 服务地址 ${esc(base)} · 生成于 ${date}

    目录

    ${API_SPECS.map((spec, p) => `

    第 ${p + 1} 部分 · ${esc(spec.title)}

      ${sections(spec).map(x => `
    1. ${esc(x.title)}
    2. `).join('')}
    `).join('\n')} ${API_SPECS.map((spec, p) => specHtml(spec, p + 1, base)).join('\n')} ` }