← 返回介绍页 /what/
XK-LAB // 公开端点清单 PUBLIC-API ● 全部只读 · 除 ask / tts 两处外都是 GET

这张表是给想自己敲命令的人看的:每个端点做什么、返回什么、有什么已知缺陷。 基址是 https://demo.xiaokeqing.cn,下面第三块那三条 curl 复制就能跑。

端点一览 · 做什么 / 返回什么 / 坑在哪17 个 · 只读
端点方法用途关键字段已知坑(先看这列)
/api/healthGET进程活着 + 线上跑的是哪个版本buildSha embeddedGitCommit vectorStore documentVectors uptimeMs status唯一端出构建指纹的端点:它和 deploy.release 对不上,就说明前端与进程不是同一次发布。无缓存(cf-cache-status: DYNAMIC),每次都要跨境回源 3–4s ⇒ 别塞进首屏必等路径。
/api/public/homeGET首屏聚合:alerts + geo + honeypot 三合一alerts geo honeypot ipDisclosure{notice, removalContact} alerts.generatedAt snapshotAt cacheAgeSeconds有缓存(45s,带 stale-while-revalidate=90)。快 ≠ 新:真实年龄 = cacheAgeSeconds + 响应头 Age;体量随 24h 数据长,实测 23–27 KB,别当固定大小的小接口。
/api/public/overviewGET主机与链路在线 + 告警窗口分布 + 数据源模式nodes[]{id, label, online} alerts{windowSize, windowCount, bySeverity[], bySource[]} dataSource{wazuhMode, degraded}nodes[].detail 里带构建提交号(实测形如 build da9d1dba…)—— 渲染只取 id/label/online,别把 detail 整块吐到页面上。另外看 wazuhMode:只有 REAL 才是真 Wazuh 在供数,降级会显式写出来。
/api/public/alertsGET告警聚合(窗口内告警 + 来源 + severity)windowSize rawTotal total aggregated list[]{aggId, count, startTime, endTime, title, ruleName, srcIp, srcGeo, severity, status, hitCount} stats24h.trend[]给出的是聚合,不是原始日志:同一份响应里 rawTotal 是原始条数、aggregated 才是聚合成几条 —— 行数对不上不是 bug,是这两列本来就不同(实测 200 → 26)。
/api/public/alerts/{aggId}GET单条告警详情该 aggId 那一条的完整结构路径里是聚合 id,不是 IP;想「按 IP 查告警」这个端点做不到,不存在的 id 直接 404。
/api/public/geoGET来源地理聚合(国家/省/ASN + 24h 趋势)coverage{totalAlerts, withSourceIp, geocoded, excludedSelf, excludedInternal} byCountry[]{country, countryName, count, uniqueIps, sharePercent} byAsnTop[] trend[] geoAttribution必须连 coverage 一起看:只有带源 IP 的才进地理统计,不许把子集画成「全量攻击」(实测 78,130 条里只有 75,127 带 IP、30,263 定位成功)。响应里零 IP;地理库是 DB-IP Lite,署名要带 geoAttribution 那行(CC BY 4.0)。
/api/public/latestGET窗口聚合(只给一个窗口的量)windowSize windowAlertCount(另有版本文档把它记成 latestEvent/latestBlock,字段以实际响应为准)窗口大小会变,别拿它和 geo.coverage.totalAlerts 直接比 —— 两个数可能不是同一个窗口。
/api/public/honeypotGET蜜罐战报(连接 / 登录 / 来源 Top / 口令结构 / 时间线)total connections logins loginFails sampled commandsAvailable topCredentials[]{username, passwordLength, passwordCharClasses, weakPassword, count} timeline[]total 恒为 10000,那是统计上限哨兵,不是真实累计 —— 真实计数看 connections(其次 logins)。connections/logins 随窗口滚动(同一晚 20:00 与 21:53 两次读数就不一样:266/268 → 267/266),对比两次要取同一时刻。口令只给用户名 + 长度 + 字符类别,永不出现原文;commandsAvailable=false 表示「命令序列这段没接数据」,不是编出来的空。
/api/public/auditGET处置审计(护栏拦截 / 封禁留痕 / 理由)summary{blockTotal, blockActive, blockedByGuard, executedActions} blocks[]{ip, durationMinutes, actor, auditId, createdAt, expiresAt, unblocked, unblockMode} actions[]{time, actor, action, target, reason, riskLevel, success} readOnly四个数都是累计,不是本周/本日;blockedByGuard 不是「封了几个」,是被护栏拦下的动作数 —— 和 blockTotal 放一起比大小是没有意义的。数据来自 data/blocked-ips.json 与 data/response-audit.json,后者不含 detail;blocks[].ip / actions[].target / reason 都可能带 IP 或内网线索,展示前一律过掩码。
/api/public/storiesGET攻击故事线候选池stageLabels{alerts, decoyPorts, honeypotLogins, auditActions} sources[]{srcIp, stages{}, stagesWithData, firstSeen, lastSeen, severityMax, gaps[]} coverage{stageStatus, stageNotes, audit{actionsReadable, actionRecords, blockRecords}} limits{defaultLimit:10, maxLimit:20, topSources:50}auditActions 多轮实测全 0 —— 不是「没有处置动作」,是这一段没读到数据,真要看读没读到去翻 coverage.audit。/api/public/stories/1 是 404,它没有子资源;sources[] 默认只给 10 条(最多 20),不是全量。
/api/public/story?src=GET单源事件时间线(点了才拉,页面不预取)events[]{ts, stage, stageLabel, kind, severity, outcome, summary}(升序)、eventCount、truncated、coverage{每段的 status/reason/queried/returned}、limits{maxEventsPerStage:40}缺 src → 400 MISSING_SRC;src 要填候选池返回的真实来源,别拿 IP 段瞎猜。每段最多 40 条,被截断时 truncated=true 如实标 —— 别把截断后的时间线当完整攻击过程。脱敏要连 events[].summary 里的内嵌 IP 一起盖。
/api/public/webstatsGETWeb 访问统计(访问量 / 来源)totals{requests, statusClass{}} topCategories[]{category, count, sharePercent} topUas[]{ua, count} heatmap[]{date, hour, count} coverage{} note一个 IP 都不给,连打码都不给;访问路径也只给固定类别、不给原文。日桶 / 状态码这两块在不同版本文档里说法不一(一份列了 byDay[] 与 statusClass,另一份实测说还没上)⇒ 渲染前先 curl 一眼,缺的格子如实写「读不到」。
/api/public/deployGET构建与发布信息(可追溯)build{} runtime{} release{release, totalBytes, source}嵌套结构,不是平铺的;没有 files / history 两个键 —— 别按它们写代码。当前线上常见 available:false + RELEASE_INFO_NOT_CONFIGURED。
/api/public/evalGET评测报告(AI 质量那几格)sampleSize keyPointHitRate refusalRate strictRate citationRate personaCatchphraseRate tabooRate generatedAt + note/caveats比率必须连 sampleSize 一起读:96 份和 4 份不是一个可信度。当前线上常见 available:false + EVAL_REPORT_NOT_CONFIGURED。
/api/public/kb?q=GET知识库只读检索rule{allowedRoots, allowedSourceTypes} results[]{title, snippet, sourceType, trustLevel, score, publicRule}、hitCount available note20 次/分钟,且每次都有 embedding 成本 ⇒ 禁止轮询。不带 q 不报错:返回空 results + note 写「未提供查询词(q)」,别把这句当检索失败。白名单口径就在响应的 rule 里:根目录 security-docs / data/kb-approved、来源类型 community、top-k=3、摘要 300 字 —— 检索不到是真没有,不是接口坏了。
/api/public/askPOST公开问答(检索 + 引用 + 六态标注)answer state/stateLabel(六态机读值)、citations[]{title, snippet, sourceType, trustLevel, score}、quota{pool, limit, used, remaining}、clientIpMasked、outputFindings、ttsTextHash、deadlineMs=20000限流压在 6 次/分钟/IP + 公开池 30 次/日 + 同 IP 并发 1,quota 会直接告诉你还剩几次。429 和「读不到」是两件事,别混着写。它只解释、不做任何处置动作;clientIpMasked 已经是打码后的值,别拿它当原始 IP 用。
/api/public/ttsPOST在线语音合成代理(转发字节流)关闭态:503 + TTS_DISABLED默认关闭(等语音 Key)。就算开了也只收白名单 phraseId / textHash,塞自由文本 → 400 TEXT_NOT_ALLOWED。

「字段」一列只写已经核对过的键,不是照抄设计稿;标了「当前 available:false」的字面意思是:端点通、数据那份文件还没配好, 这是如实报告,不是接口坏了。
字段名来自两份交接文档交叉核对(10-15 的逐端点 schema 表 + 10-17 的现状表):/overview、/latest、/webstats 三处两边记法有出入,已在「已知坑」里逐条标出 —— 写代码前先 curl 一次实际响应,比信这张表稳。

同一份数据的 .js 别名边缘可缓存 · 不吃 20 次/分钟

上面每个 /api/public/* 都有同名的 .js 别名(/api/public/alerts.js 这样)。它存在的理由是缓存: Cloudflare 按扩展名决定要不要缓存,无扩展名的 /api/public/alerts 必然是 cf-cache-status: DYNAMIC、每次都回源(实测 0.94~2.60s); 别名则命中边缘缓存,而命中缓存的请求根本到不了源站,也就不吃公开面每 IP 20 次/分钟的限流 —— 一个 /alerts/ 页面一加载就并发打 9 个端点,刷两次就超额,站主看到的「读不到」多半是这个原因。 缓存时长各端点不同(多数 45 秒,stories 15 秒),响应带 ETag、放行 Age。

两条纪律:① 别给这些地址加随机 cache-buster(?cb= / ?t= / Date.now())—— 那样每次都是新 URL,100% 回源; ② ?q= / ?src= 这类语义参数是允许的(它换来的是不同内容)。

响应体是什么:现网前端(api-bind.html,1600 行)对别名一律走 res.json(),注释里写死了这条契约 —— 「别名响应仍是 application/json,照常当 JSON 解析,别当脚本执行」。另有旧文档说它是 window.X = … 的脚本形式、可直接 <script src> 引入,与上述契约和现网前端口径不符,本页按契约写。一条命令可自证:

# -D - 把响应头一起打出来:看 content-type 是不是 application/json、体开头是不是 {
curl -s -D - https://demo.xiaokeqing.cn/api/public/home.js | head -c 300
怎么最快验证一个端点三条 curl · 只读 · 不写入
# ① 活着吗、线上跑的是哪个版本(表格第 1 行)
curl -s https://demo.xiaokeqing.cn/api/health | head -c 300

# ② 蜜罐:total 恒为 10000 是上限哨兵,真值看 connections(表格第 8 行)
curl -s https://demo.xiaokeqing.cn/api/public/honeypot.js | head -c 400

# ③ 限流长什么样:连打 3 次 kb(20 次/分钟按 IP 算,打满后就是 429)
for i in 1 2 3; do curl -s -o /dev/null -w '%{http_code} ' "https://demo.xiaokeqing.cn/api/public/kb?q=rate-limit"; done; echo

# Windows PowerShell 等价写法(本页就是在这台机器上写的):
#  Invoke-WebRequest -UseBasicParsing https://demo.xiaokeqing.cn/api/health | % Content
#  curl.exe -s -o NUL -w "%{http_code} " "https://demo.xiaokeqing.cn/api/public/kb?q=probe"

三条都只读,不写库、不改状态、不触发任何处置动作。取不到的时候(断网、429、源站挂了)就是取不到 —— 这一页不打算替换成任何「样例值」,/status/ 那页才是「现在通不通」的实测表。

口径与边界公开面只读

① 只读。所有公开端点都没有写操作;ask / tts 虽是 POST,语义仍是「问一句 / 合成一段」,不改任何状态。 要处置(封 IP、审批)得走受保护的自用面:管理员令牌 + 人工审批。
② 地址不全给。geo 与 webstats 的响应里零 IP;alerts / story 会带 IP, 但展示层统一掩成 1.2.3.x(ask 干脆自带 clientIpMasked)。curl 直取时可能看到未掩码的原文 —— 显示层掩码与接口原文不一致属于预期,原文只留在 srv1 日志里。
③ 失败说失败。取不到就写「未取到 / 读不到」并带原因(available:false + reason),不补 0、不补样例值。
④ 不常驻。没有 SSE / WebSocket,一次性返回;要新数据就再敲一次。

要复现「现在通不通」的逐端点体检看 状态页 /status/;要复现这个站是干什么的看 介绍页 /what/。口径细节写在笔记里: 公开接口防刷 · 可验证的发布链