官网/docs 嵌入新品牌图;sparkapi nginx 可在未重建镜像时提供 /favicon.ico 与页头 logo。 Co-authored-by: Cursor <cursoragent@cursor.com>
299 lines
12 KiB
HTML
299 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<title>Spark IM API</title>
|
||
<link rel="icon" href="/favicon.ico?v=20260924-flames" sizes="any"/>
|
||
<link rel="icon" href="/docs/static/favicon-32.png?v=20260924-flames" type="image/png" sizes="32x32"/>
|
||
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=20260924-flames"/>
|
||
<script>
|
||
(function () {
|
||
var p = location.pathname.replace(/\/+$/, "") || "/docs";
|
||
var k = p === "/docs/enums" ? "enums" : p === "/docs/errors" ? "errors" : "api";
|
||
document.documentElement.setAttribute("data-page", k);
|
||
})();
|
||
</script>
|
||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css"/>
|
||
<style>
|
||
body { margin: 0; font-family: sans-serif; color: #3b4151; }
|
||
a { color: #3b82f6; }
|
||
a:hover { text-decoration: underline; }
|
||
.offline { padding: 16px; }
|
||
.docs-bar {
|
||
display: flex; align-items: center; gap: 24px; flex-wrap: wrap;
|
||
padding: 10px 20px; border-bottom: 1px solid #e0e0e0; background: #fafafa;
|
||
position: sticky; top: 0; z-index: 20;
|
||
}
|
||
.docs-brand {
|
||
display: inline-flex; align-items: center; gap: 8px; color: #3b4151;
|
||
font-weight: 700; text-decoration: none;
|
||
}
|
||
.docs-brand img {
|
||
width: 28px; height: 28px; border-radius: 6px; display: block;
|
||
}
|
||
.docs-bar strong { color: #3b4151; }
|
||
.docs-nav { display: flex; gap: 20px; margin: 0; padding-left: 22px; }
|
||
.docs-nav a { color: #3b4151; text-decoration: none; }
|
||
html[data-page="api"] #nav-api,
|
||
html[data-page="enums"] #nav-enums,
|
||
html[data-page="errors"] #nav-errors { color: #3b82f6; font-weight: 600; }
|
||
.docs-bar select { font: inherit; }
|
||
.page { display: none; }
|
||
html[data-page="api"] #page-api,
|
||
html[data-page="enums"] #page-enums,
|
||
html[data-page="errors"] #page-errors { display: block; }
|
||
.catalog { max-width: 1100px; padding: 16px 20px 48px; }
|
||
.catalog h1 { margin: 0 0 8px; font-size: 22px; }
|
||
.lead { margin: 0 0 16px; color: #6b7280; font-size: 14px; }
|
||
.layout { display: grid; grid-template-columns: 220px 1fr; gap: 24px; align-items: start; }
|
||
.toc {
|
||
position: sticky; top: 56px; border: 1px solid #e5e7eb; border-radius: 8px;
|
||
padding: 12px; background: #fff; max-height: calc(100vh - 80px); overflow: auto;
|
||
}
|
||
.toc h2 { margin: 0 0 8px; font-size: 13px; }
|
||
.toc ol { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; }
|
||
.toc a { color: #3b4151; text-decoration: none; }
|
||
.toc a:hover { color: #3b82f6; }
|
||
.set { scroll-margin-top: 64px; margin-bottom: 24px; }
|
||
.set h2 { margin: 0 0 8px; font-size: 16px; }
|
||
.set h2 a { color: inherit; text-decoration: none; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th, td { border: 1px solid #d8d8d8; padding: 6px 8px; text-align: left; }
|
||
th { background: #f3f3f3; }
|
||
code { font-size: 12px; }
|
||
.err-row { scroll-margin-top: 64px; }
|
||
.err-row:target { background: #fff7ed; }
|
||
.set:target { outline: 2px solid #93c5fd; outline-offset: 4px; border-radius: 4px; }
|
||
.swagger-jump {
|
||
margin: 16px 20px 0; padding: 12px 16px 12px 36px;
|
||
border: 1px solid #e5e7eb; border-radius: 8px; background: #fff;
|
||
line-height: 1.8;
|
||
}
|
||
.swagger-jump a { font-weight: 600; }
|
||
.swagger-ui .markdown table,
|
||
.swagger-ui table {
|
||
table-layout: auto;
|
||
width: 100%;
|
||
}
|
||
.swagger-ui .markdown table th,
|
||
.swagger-ui .markdown table td,
|
||
.swagger-ui table th,
|
||
.swagger-ui table td {
|
||
white-space: normal !important;
|
||
word-break: break-word;
|
||
overflow-wrap: anywhere;
|
||
vertical-align: top;
|
||
}
|
||
.swagger-ui table.tcp-cmd th:nth-child(3),
|
||
.swagger-ui table.tcp-cmd td:nth-child(3) {
|
||
min-width: 8em;
|
||
white-space: nowrap !important;
|
||
}
|
||
.swagger-ui .markdown details {
|
||
margin: 12px 0;
|
||
padding: 8px 12px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
.swagger-ui .markdown details summary {
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
}
|
||
@media (max-width: 800px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
.toc { position: static; max-height: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="docs-bar">
|
||
<a class="docs-brand" href="/docs">
|
||
<img src="/docs/static/logo.png?v=20260924-flames" width="28" height="28" alt=""/>
|
||
<strong>Spark IM</strong>
|
||
</a>
|
||
<ol class="docs-nav">
|
||
<li><a id="nav-api" href="/docs">API</a></li>
|
||
<li><a id="nav-enums" href="/docs/enums">enums</a></li>
|
||
<li><a id="nav-errors" href="/docs/errors">errors</a></li>
|
||
</ol>
|
||
<a href="/openapi.yaml">openapi.yaml</a>
|
||
<label>Language
|
||
<select id="lang">
|
||
<option value="zh-CN">zh-CN</option>
|
||
<option value="en">en</option>
|
||
<option value="zh-TW">zh-TW</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<section id="page-api" class="page">
|
||
<ol class="swagger-jump">
|
||
<li><a href="/docs/enums">enums</a> — 类型 / 状态枚举</li>
|
||
<li><a href="/docs/errors">errors</a> — 错误码 envelope.code</li>
|
||
</ol>
|
||
<div id="swagger-ui"></div>
|
||
</section>
|
||
|
||
<section id="page-enums" class="page">
|
||
<div class="catalog">
|
||
<h1>Enums</h1>
|
||
<p class="lead">与 JSON / TCP / SQL 同一套 <code>value</code>。<code>name</code> 不随语言变化。<code>label</code> 随右上角语言变化。数据来自 <a href="/v1/meta/enums">GET /v1/meta/enums</a>。点左侧目录跳转。</p>
|
||
<div class="layout">
|
||
<nav class="toc" id="enum-toc"></nav>
|
||
<div id="enum-sets"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="page-errors" class="page">
|
||
<div class="catalog">
|
||
<h1>错误码</h1>
|
||
<p class="lead">HTTP 与 TCP Body 共用 <code>{"code","msg","request_id","data"}</code>。<code>code</code> 是字符串,只有 <code>"0"</code> 成功。业务失败也是 HTTP 200,看 <code>code</code> 不要只看状态码(<code>/readyz</code> 未就绪才是 503)。<code>request_id</code> 由服务端生成,从响应体读取。点目录可跳到对应码。</p>
|
||
<div class="layout">
|
||
<nav class="toc" id="error-toc"></nav>
|
||
<div>
|
||
<table>
|
||
<thead><tr><th>code</th><th>name</th><th>msg</th></tr></thead>
|
||
<tbody id="error-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<noscript>
|
||
<div class="offline">JavaScript 未开启。规范:<a href="/openapi.yaml">/openapi.yaml</a>;枚举:<a href="/v1/meta/enums">/v1/meta/enums</a>。</div>
|
||
</noscript>
|
||
<script>
|
||
function pageKind() {
|
||
return document.documentElement.getAttribute("data-page") || "api";
|
||
}
|
||
function detectLang() {
|
||
var q = new URLSearchParams(location.search).get("lang");
|
||
if (q === "zh-CN" || q === "en" || q === "zh-TW") return q;
|
||
var n = (navigator.language || "").toLowerCase();
|
||
if (n.indexOf("zh-tw") === 0 || n.indexOf("zh-hk") === 0 || n.indexOf("zh-hant") === 0) return "zh-TW";
|
||
if (n.indexOf("en") === 0) return "en";
|
||
return "zh-CN";
|
||
}
|
||
function esc(s) {
|
||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
function currentLang() {
|
||
return document.getElementById("lang").value;
|
||
}
|
||
function withLang(href) {
|
||
return href + (href.indexOf("?") >= 0 ? "&" : "?") + "lang=" + encodeURIComponent(currentLang());
|
||
}
|
||
function jumpHash() {
|
||
if (!location.hash) return;
|
||
var el = document.getElementById(location.hash.slice(1));
|
||
if (el) el.scrollIntoView();
|
||
}
|
||
function loadCatalog() {
|
||
var lang = currentLang();
|
||
document.documentElement.lang = lang;
|
||
var kind = pageKind();
|
||
if (kind === "api") return;
|
||
fetch("/v1/meta/enums", { headers: { "X-Language": lang, "Accept-Language": lang } })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (env) {
|
||
var data = env && env.data ? env.data : {};
|
||
if (kind === "enums") renderEnums(data);
|
||
else renderErrors(data.envelope_code || []);
|
||
jumpHash();
|
||
})
|
||
.catch(function () {
|
||
var box = kind === "enums" ? document.getElementById("enum-sets") : document.getElementById("error-rows");
|
||
box.innerHTML = '<p class="offline">无法加载 <a href="/v1/meta/enums">/v1/meta/enums</a>。</p>';
|
||
});
|
||
}
|
||
function renderEnums(data) {
|
||
var keys = Object.keys(data).filter(function (k) { return k !== "envelope_code"; });
|
||
var toc = document.getElementById("enum-toc");
|
||
var box = document.getElementById("enum-sets");
|
||
if (!keys.length) {
|
||
toc.innerHTML = "";
|
||
box.innerHTML = '<p class="offline">empty catalog</p>';
|
||
return;
|
||
}
|
||
var tocHtml = "目录<ol>";
|
||
var html = "";
|
||
keys.forEach(function (k) {
|
||
var id = "enum-" + k;
|
||
tocHtml += '<li><a href="#' + id + '"><code>' + esc(k) + "</code></a></li>";
|
||
var rows = data[k] || [];
|
||
html += '<article class="set" id="' + id + '"><h2><a href="#' + id + '"><code>' + esc(k) + "</code></a></h2>";
|
||
html += "<table><thead><tr><th>value</th><th>name</th><th>label</th></tr></thead><tbody>";
|
||
rows.forEach(function (it) {
|
||
html += "<tr><td><code>" + esc(it.value) + "</code></td><td><code>" + esc(it.name) + "</code></td><td>" + esc(it.label) + "</td></tr>";
|
||
});
|
||
html += "</tbody></table></article>";
|
||
});
|
||
toc.innerHTML = tocHtml + "</ol>";
|
||
box.innerHTML = html;
|
||
}
|
||
function renderErrors(rows) {
|
||
var toc = document.getElementById("error-toc");
|
||
var body = document.getElementById("error-rows");
|
||
if (!rows.length) {
|
||
toc.innerHTML = "";
|
||
body.innerHTML = '<tr><td colspan="3">empty catalog</td></tr>';
|
||
return;
|
||
}
|
||
var tocHtml = "目录<ol>";
|
||
var html = "";
|
||
rows.forEach(function (it) {
|
||
var id = "code-" + String(it.value);
|
||
tocHtml += '<li><a href="#' + id + '"><code>' + esc(it.value) + "</code> " + esc(it.name) + "</a></li>";
|
||
html += '<tr class="err-row" id="' + id + '"><td><code>' + esc(it.value) + "</code></td><td><code>"
|
||
+ esc(it.name) + "</code></td><td>" + esc(it.label) + "</td></tr>";
|
||
});
|
||
toc.innerHTML = tocHtml + "</ol>";
|
||
body.innerHTML = html;
|
||
}
|
||
function bootSwagger() {
|
||
if (pageKind() !== "api") return;
|
||
if (!window.SwaggerUIBundle) {
|
||
document.getElementById("swagger-ui").innerHTML =
|
||
'<div class="offline">Swagger UI 未加载。规范:<a href="/openapi.yaml">/openapi.yaml</a></div>';
|
||
return;
|
||
}
|
||
window.SwaggerUIBundle({
|
||
url: "/openapi.yaml",
|
||
dom_id: "#swagger-ui",
|
||
presets: [SwaggerUIBundle.presets.apis],
|
||
persistAuthorization: true,
|
||
onComplete: function () {
|
||
document.querySelectorAll(".swagger-ui table").forEach(function (t) {
|
||
var txt = (t.textContent || "");
|
||
if (txt.indexOf("AUTH") >= 0 && txt.indexOf("SEND_MESSAGE") >= 0) t.classList.add("tcp-cmd");
|
||
});
|
||
},
|
||
requestInterceptor: function (req) {
|
||
req.headers["X-Language"] = currentLang();
|
||
if (!req.headers["X-App-Version"]) req.headers["X-App-Version"] = "docs-ui";
|
||
return req;
|
||
}
|
||
});
|
||
}
|
||
document.getElementById("lang").value = detectLang();
|
||
document.getElementById("nav-api").href = withLang("/docs");
|
||
document.getElementById("nav-enums").href = withLang("/docs/enums");
|
||
document.getElementById("nav-errors").href = withLang("/docs/errors");
|
||
var enumJump = document.querySelector(".swagger-jump a[href='/docs/enums']");
|
||
var errJump = document.querySelector(".swagger-jump a[href='/docs/errors']");
|
||
if (enumJump) enumJump.href = withLang("/docs/enums");
|
||
if (errJump) errJump.href = withLang("/docs/errors");
|
||
document.getElementById("lang").onchange = function () {
|
||
var u = new URL(location.href);
|
||
u.searchParams.set("lang", currentLang());
|
||
location.href = u.pathname + u.search + u.hash;
|
||
};
|
||
loadCatalog();
|
||
</script>
|
||
<script src="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js" onload="bootSwagger()" onerror="bootSwagger()"></script>
|
||
</body>
|
||
</html>
|