项目:XSearch | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
《03-接口设计》讲契约的设计意图与鉴权时序,《15-接口全量参考》讲全量 29 条路由的参数边界与错误码。本篇做第三件事:给出可以直接复制执行的真实请求/响应示例——每条核心接口配 curl 命令、成功响应 JSON、错误响应 JSON,字段形状严格按 XSearch/search/api_views.py 的视图实现与 frontend/src/api.ts 的 interface 构造,最后附一个真实的 ES 查询 DSL。选入的 9 组接口覆盖系统全部主干:搜索(#1/#2)、热搜与概览(#3)、要闻(#4)、榜单与新闻列表(#5)、详情(#6)、登录与 token(#7)、采集任务管理(#8)、数据管理(#9)。
示意值约定:所有示例中的具体取值——id 的 MD5 串、时间戳、token、数字统计——均为示意,仅字段名与结构与源码逐一对齐;id 字段是 ES 文档 _id(本项目中为 md5(url) 或站点原生 id,如 aihot_8f2b…、douyin_hot_1)。开发态直连后端用 localhost:8000;生产态经前端 nginx 反代同源(https://你的域名)。管理接口统一携带 X-Admin-Token 请求头。错误响应的完整枚举(401/429/409/413/503 等)见《15-接口全量参考》第九节总表,本篇只在每组接口旁列出该接口特有的错误形态。
示例的构造方法值得先交代一句,因为它决定了本篇可以放心当作联调参照:每条响应示例不是凭记忆写的,而是把 search/api_views.py 对应视图函数的返回字典逐键抄录、再把可选字段按实际分支补齐(比如搜索响应里 did_you_mean/corrected/fuzzy/suggestions 四个纠错字段恒在、恒为对应类型);条目形状则对照 frontend/src/api.ts 的 DocItem/SearchResult 等 interface 双向核对——两端一致的字段才是契约,只在单侧出现的(如后端 ServiceInfo 的 endpoints 列表)会特别注明。反过来,⚠️ 待确认 的位置也用同一方法确定:任何一处后端字段与前端 interface 形状有出入的地方(如 DbPurgeResult 里前端多声明的 source 可选键),都按后端实现为准并在文中标注。
在逐条看示例之前,有四组横切约定需要先立住,它们决定了任何一个接口在异常时刻的行为,也是对接新客户端时最先要实现的分支。第一是鉴权:全站只有一层管理员 token,公开读接口(搜索、补全、概览、要闻、榜单、详情、图片代理、SEO)完全无鉴权;管理接口统一经 require_admin 装饰器校验 X-Admin-Token 头,失败一律 401 加 code: "auth_required",不存在"部分保护"的中间态。第二是错误体形状:统一为 {"error": "<中文可直接展示的文案>", "code": "<机器码,可选>"}——文案给前端直接渲染,机器码给程序分支用,且机器码全集只有三个(auth_required/not_found/too_many_login_attempts),新增错误场景时优先复用既有状态码语义而非发明新码。第三是依赖降级:ES 不可用时读接口返回结构化 503(_es_error() 统一产出),唯 suggest 与 stats 两个接口降级为 200(前者返回空数组、后者置 es_ok:false);Redis 不可用时所有接口静默跳过缓存与统计照常出数据——对接方收到 2xx 仍应检查业务降级字段。第四是参数夹取:p 非法回退 1、hours 夹 1-168、pages 夹 1-20、limit 夹 1-200,越界不报错而是收敛到合法区间,这是"宽容入口"的一致风格(白名单类参数除外——sort/days/source/spider 走严格白名单,不合法直接拒绝)。
curl "http://localhost:8000/api/search/?q=%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD&sort=time&days=30&p=1"成功响应(200):
{
"total": 128,
"page": 1,
"page_nums": 13,
"results": [
{
"id": "3f5a8c2e9d1b4a7f6e0c8d2b5a9f1e3c",
"title": "<span class=\"kw\">AI</span> 大模型发布会举行:多模态能力全面升级",
"content": "…<span class=\"kw\">人工智能</span>…生成式模型迎来新一轮迭代,",
"url": "https://www.ithome.com/0/123/456.htm",
"author": "IT之家",
"source": "news_ithome",
"rating": null,
"rank": null,
"front_image_url": "",
"create_date": "2026-10-05",
"praise_nums": null,
"view_nums": null,
"reply_nums": null,
"danmaku_nums": null
}
],
"facets": {
"sources": [
{"key": "news_ithome", "count": 52},
{"key": "news_chinanews", "count": 41},
{"key": "aihot_news", "count": 35}
],
"days": [
{"key": "7", "count": 30},
{"key": "30", "count": 128},
{"key": "90", "count": 400}
]
},
"did_you_mean": null,
"corrected": null,
"fuzzy": false,
"suggestions": []
}契约要点:results[].title/content 中的 <span class="kw">…</span> 是 ES 高亮片段(唯一携带 HTML 的响应内容),前端 renderHighlight() 整体转义后仅还原为 <mark class="kw">,对接方若自行渲染必须做同等转义;facets.days 的三个桶是累计口径("30" 桶包含 7 天内的文档),分面计数不受当前 source/days 筛选影响(post_filter 语义,见《05-核心流程》流程二);corrected 非空表示已自动换词重搜(黄色提示)、fuzzy: true 表示模糊重搜(蓝色提示)、suggestions 仅在空结果时携带 Redis 热搜词 Top6。特有错误:ES 故障返回 503 {"error": "Elasticsearch 不可用: <异常>"};q 为空时返回 200 与空结果集(不查 ES)。
curl "http://localhost:8000/api/suggest/?s=%E4%BA%BA%E5%B7%A5"成功响应(200,JSON 数组):
["人工智能芯片格局生变", "人工智能通识课进校园", "AI 与人工智能安全治理"]契约要点:返回的是标题字符串数组而非对象数组(JsonResponse(titles, safe=False)),来源为 ES suggest completion 字段(input 来自 title 与 author)+ fuzziness 2 容错,保序去重且已排除榜单来源。两条降级约定:s 为空返回 [];ES 故障也静默降级为 200 [](补全是最不该报错的接口)。前端 250ms 防抖 + suggestSeq 竞态序号消费(《02-页面设计》4.2)。
curl "http://localhost:8000/api/stats/"成功响应(200):
{
"es_ok": true,
"total": 23417,
"by_source": [
{"key": "news_chinanews", "count": 9821},
{"key": "news_ithome", "count": 6120},
{"key": "news_solidot", "count": 3120},
{"key": "aihot_news", "count": 2104},
{"key": "douyin_hot", "count": 50},
{"key": "aihot_hot", "count": 10}
],
"top_keywords": ["人工智能", "大模型", "芯片", "新能源车", "vivo", "诺贝尔奖", "脑机接口", "折叠屏"]
}契约要点:top_keywords 即热搜词(Redis search_keywords_set zset 按 score 降序 Top8,搜索行为实时累积);by_source 是全库来源分布(size 20)。关键约定是降级语义:ES 故障时仍返回 200,改为 {"es_ok": false, "es_error": "<截断 200 字>", "total": 0, …}——README 特别提醒该接口不能当容器探针(降级 200 会让依赖故障隐形),探针请用 /api/health/(其响应 {"status":"ok"|"degraded", "checks":{"elasticsearch":"ok"|"异常类名"}, "index":{"name":"quotes","docs":23417}},任一依赖挂 503 且只回异常类名不回消息串)。
curl "http://localhost:8000/api/headlines/?hours=36"成功响应(200):
{
"hours": 36,
"total": 20,
"items": [
{
"id": "5c9e2f8a1d3b7e6f4a0c2d8e9f1a3b5c",
"rank": null,
"title": "神舟二十号载人飞船发射圆满成功",
"content": "据中国载人航天工程办公室消息,北京时间今日……(新闻源截 150 字)",
"url": "https://www.chinanews.com.cn/gn/2026/10-05/10234567.shtml",
"author": "中国新闻网",
"source": "news_chinanews",
"rating": null,
"front_image_url": "",
"view_nums": null,
"praise_nums": null,
"danmaku_nums": null,
"reply_nums": null,
"create_date": "2026-10-05"
},
{
"id": "aihot_9f2b7e4a",
"title": "OpenAI 发布新一代推理模型",
"content": "AIHOT:今日最值得关注的事件是多信源印证的……(AI 源保留全文)",
"url": "https://aihot.news/topics/9f2b7e4a",
"author": "AIHOT",
"source": "aihot_news",
"rating": 87.5,
"create_date": "2026-10-05"
}
],
"top_keywords": ["人工智能", "大模型", "芯片"]
}契约要点:条目字段即 _list_item() 的统一映射(与榜单/新闻列表同构),id 供详情页精确取文;AI 来源条目保留全文(keep_content=True),新闻条目截 150 字——消费方按 source 判断是否站内详情(AI 三源跳 /ai/detail?id=,新闻外链源站,前端 HomeView.isAi() 的分流逻辑);同题去重(标题去空白前 40 字为键)后最多 20 条;hours 夹 1-168 默认 36。整体 payload 有 120 秒 Redis 缓存(键 headlines:{hours}),ES 故障返回 503。
curl "http://localhost:8000/api/rankings/?source=douyin_hot"
curl "http://localhost:8000/api/rankings/?source=news&p=1"成功响应——榜单分支(source=douyin_hot/aihot_hot,按 rank 升序一次拉全):
{
"source": "douyin_hot",
"total": 50,
"items": [
{
"id": "douyin_hot_1",
"rank": 1,
"title": "国庆假期全国出行数据发布",
"content": "抖音热点榜第1名:国庆假期全国出行数据发布",
"url": "https://www.douyin.com/search/国庆假期全国出行数据发布",
"author": "抖音热点",
"source": "douyin_hot",
"rating": null,
"front_image_url": "",
"view_nums": 12345600,
"create_date": "2026-10-05"
}
]
}成功响应——新闻分支(source=news/三新闻源/aihot_daily,时间倒序 20 条/页):
{
"source": "news",
"total": 19041,
"page": 1,
"page_nums": 953,
"items": [
{
"id": "3f5a8c2e9d1b4a7f6e0c8d2b5a9f1e3c",
"rank": null,
"title": "某科技公司发布新一代旗舰芯片",
"content": "(摘要,截 150 字)……",
"url": "https://www.ithome.com/0/123/789.htm",
"author": "IT之家",
"source": "news_ithome",
"create_date": "2026-10-05"
}
]
}契约要点:两种分支响应形状不同(榜单分支无 page/page_nums,消费方按 source 区分);view_nums 是"语义复用"字段(抖音存热度值、aihot_hot 存信源印证数);新闻分支有 60 秒 Redis 缓存(键 rankings_news:{source}:p{page})。特有错误:source 不在合法集合(aihot_hot/douyin_hot/news/三新闻源/aihot_daily)返回 400 {"error": "不支持的榜单来源"}。
curl "http://localhost:8000/api/doc/aihot_9f2b7e4a/"成功响应(200,_list_item(keep_content=True) 全文形态):
{
"id": "aihot_9f2b7e4a",
"rank": null,
"title": "OpenAI 发布新一代推理模型",
"content": "AIHOT 综述:过去 24 小时,多信源共同印证的事件是……(全文,上限 5000 字)",
"url": "https://aihot.news/topics/9f2b7e4a",
"author": "AIHOT",
"source": "aihot_news",
"rating": 87.5,
"front_image_url": "",
"view_nums": 42,
"praise_nums": null,
"danmaku_nums": null,
"reply_nums": null,
"create_date": "2026-10-05"
}契约要点:详情页是唯一"全文不截断"的读接口(列表 150/300 字、详情 5000 上限);create_date 已换算为站点时区(Asia/Shanghai)的 YYYY-MM-DD。特有错误:_id 不存在返回 404 {"error": "文章不存在或已被清理", "code": "not_found"}——机器码 not_found 是全站仅有的三个错误码之一;ES 故障 503。该接口取代了旧的"按标题猜文"端点(防错链,见《18-代码走读实录》2.3)。
curl -X POST "http://localhost:8000/api/auth/login/" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "correct-password"}'成功响应(200):
{
"ok": true,
"token": "8f14e45fceea167a5a36dedd4bea2543c2b1e9a7d5f3c1b9e7a5d3f1c9b7e5a3",
"username": "admin",
"expires_in": 43200
}错误响应(四种,状态码即语义):
{"error": "用户名或密码错误"}{"error": "失败次数过多,请 812 秒后重试", "code": "too_many_login_attempts"}{"error": "管理员账号未配置:请创建 XSearch/local_settings.py(参考 local_settings.py.example)"}契约要点:token 为 64 位十六进制(secrets.token_hex(32),256 bit 熵),此后所有管理接口以 X-Admin-Token 头携带;expires_in: 43200(12 小时)且每次成功请求滑动续期;锁定分支(429)带 Retry-After 头(秒数,直接可 setTimeout),且先于密码比较执行——密码正确也照样 429,自动化脚本不要把 429 当"密码错误"做字典尝试;账号未配置返回 503 而非 500(配置缺失不是程序崩溃,测试固化)。登出 POST /api/auth/logout/(携带要吊销的 token)幂等返回 {"ok": true}。token 存进程内字典:后端重启全部失效需重登(单 worker 架构的既定代价,见 ADR-04 与《16-决策记录ADR》)。
把 token 的完整生命周期按"四问"过一遍,对接方就能覆盖全部边界。它存在哪:后端进程内字典 _admin_tokens(token 到过期时间戳),不落盘、不入库、不进任何响应体之外的地方——因此没有"查所有 token"的管理接口,丢了只能等过期或重启。它什么时候失效:三种路径——超时(12 小时无请求)、登出(从字典弹出)、进程重启(字典清空);服务端在校验失败路径上还会顺带清理全部过期 token,长期运行不积累。它怎么续期:每次成功的管理请求都把过期时间重置为"当前时刻加 12 小时",所以"12 小时"指的是最后 activity 而非首次登录;前端无需刷新逻辑,只有 401 才需要重新登录。它在传输中怎么保护:走 HTTPS(VPS Nginx 终结)、以请求头而非 Cookie 携带(天然免疫 CSRF,这也是写接口豁免 CSRF 校验的前提——token 永不落入 Cookie)。
curl -X POST "http://localhost:8000/api/crawl/start/" \
-H "Content-Type: application/json" \
-H "X-Admin-Token: 8f14e45fceea167a5a36dedd4bea2543..." \
-d '{"spider": "news_rss", "pages": 2, "js": false}'成功响应(202,任务已异步启动):
{
"started": true,
"pid": 41237,
"started_at": "2026-10-06T09:30:00",
"spider": "news_rss",
"label": "新闻RSS(3源)",
"job_dir": "news_rss_1788987390",
"resumed": false
}冲突响应(409,已有任务在运行——这是正常业务态,监控不应计入错误率):
{
"started": false,
"reason": "已有采集任务在运行",
"pid": 41201,
"started_at": "2026-10-06T09:28:11"
}契约要点:spider 必须在白名单 SPIDERS(douyin_hot/aihot_hot/aihot_news/news_rss/news_backfill,可用 GET /api/crawl/spiders/ 查询),未知爬虫同样 409(reason: "未知爬虫: …");pages 夹 1-20 默认 2;带 resume_job 时为 JOBDIR 目录名(经 JOB_NAME_RE 三重校验,见《18-代码走读实录》3.2),非法返回 409 "非法的任务名称"。启动是异步的:202 只表示子进程已拉起,终态由盯梢线程回写(无轮询依赖)。
异步模型值得对接方多理解一层,因为它决定了客户端该怎么消费这个接口。调用方拿到 202 后不需要也不应该长轮询等待终态——采集是分钟级的长任务,正确的消费姿势是按需查询两个只读端点:/api/crawl/status/ 给"现在正跑着谁、日志尾部长什么样",/api/crawl/history/ 给"过去的每一轮跑成了什么样"。前端采集管理页的做法可以照抄:运行中每 2 秒查一次 status,空闲时降到 8 秒,页面卸载清掉定时器。409 冲突响应里附带的 pid 与 started_at 不是装饰——前端用它展示"正在运行的那个任务是谁",对接方做并发控制时也应利用这两个字段判断"是不是同一个任务还在跑"。此外全系统同时最多一个采集任务,第二个请求无论指向哪个爬虫都会得到 409,这是互斥锁的语义而不是竞态缺陷。
curl "http://localhost:8000/api/crawl/status/" -H "X-Admin-Token: 8f14..."
curl "http://localhost:8000/api/crawl/history/?limit=50" -H "X-Admin-Token: 8f14..."status 响应(200):
{
"running": false,
"status": "completed",
"started_at": "2026-10-06T09:30:00",
"spider": "news_rss",
"log_tail": [
"[2026-10-06 09:30:01] [中国新闻网] feed 解析出 48 条",
"[2026-10-06 09:30:03] [IT之家] feed 解析出 30 条",
"…(最多 30 行)…",
"'item_scraped_count': 76, 'finish_reason': 'finished'"
]
}history 响应(200,最新在前):
{
"history": [
{
"job_id": "job_1788987390123",
"spider": "news_rss",
"status": "completed",
"started_at": "2026-10-06T09:30:00",
"ended_at": "2026-10-06T09:31:12",
"pid": 41237,
"log_path": "/app/logs/crawl_1788987390.log",
"job_dir": "news_rss_1788987390",
"resumed": false,
"returncode": 0,
"items": 76
}
]
}契约要点:status 五态机 running → completed / failed / empty / interrupted(empty 是"进程正常退出但 0 条",风控/改版信号,会触发冷却告警邮件;interrupted 由重启收尾 _reconcile_stale_runs 判定);items 从日志正则解析 'item_scraped_count': N。status 含服务器绝对路径(log_tail),因此必须是管理员接口。另有 GET /api/crawl/stats/(成功率 + by_day 最近 14 天 + by_spider 聚合)与 GET /api/crawl/resumable/(可恢复 JOBDIR 列表,含 has_queue/size_kb),形状见《15-接口全量参考》第四节。
curl -X POST "http://localhost:8000/api/crawl/schedule/add/" \
-H "Content-Type: application/json" \
-H "X-Admin-Token: 8f14..." \
-d '{"spider": "news_rss", "cron": "0 8 * * *", "pages": 2}'成功响应(201):
{"ok": true, "job_id": "job_1789046400123", "scheduler_active": true}list 响应(200):
{
"jobs": [
{
"id": "job_1789046400123",
"spider": "news_rss",
"label": "新闻RSS(3源)",
"cron": "0 8 * * *",
"pages": 2,
"js": false,
"enabled": true,
"created_at": "2026-10-06T09:30:00",
"next_run": "2026-10-07 08:00:00",
"last_fire": "2026-10-06T08:00:01",
"last_status": "started",
"last_reason": ""
}
],
"history": [
{"job_id": "job_1789046400123", "spider": "news_rss", "time": "2026-10-06T08:00:01", "status": "started", "reason": ""}
],
"scheduler_active": true
}契约要点:cron 为五段"分 时 日 月 周",经三级校验(段数 → [\d*/,\-]+ 字符集 → CronTrigger.from_crontab 语义,"0 25 * * *" 这类越界会被 400 拒绝,错误体 {"ok": false, "reason": "cron 表达式不合法: …"});last_status 三态 started/skipped/misfired(外加 error)——misfired 与 skipped 都会如实暴露,"从没跑过看不出来"的问题由此终结(《05-核心流程》流程三);scheduler_active: false 表示 APScheduler 未安装(配置仍保存但不会自动触发,附 warning)。落盘失败的语义是"如实告知":{ok: false, reason: "配置写入失败……重启后回到旧值"},消费方应展示而非静默重试。update/remove/toggle 三端点的入参为 {job_id, ...},形状同上。
curl "http://localhost:8000/api/admin/db/docs/?source=news_ithome&p=1" -H "X-Admin-Token: 8f14..."
curl -X PUT "http://localhost:8000/api/admin/db/doc/aihot_9f2b7e4a/" \
-H "Content-Type: application/json" -H "X-Admin-Token: 8f14..." \
-d '{"title": "OpenAI 发布新一代推理模型(修订)"}'
curl -X POST "http://localhost:8000/api/admin/db/purge/" \
-H "Content-Type: application/json" -H "X-Admin-Token: 8f14..." \
-d '{"source": "news_people", "confirm": "news_people"}'docs 响应(200):
{
"total": 6120,
"page": 1,
"page_size": 20,
"page_nums": 306,
"items": [
{
"id": "3f5a8c2e9d1b4a7f6e0c8d2b5a9f1e3c",
"title": "某科技公司发布新一代旗舰芯片",
"source": "news_ithome",
"author": "IT之家",
"url": "https://www.ithome.com/0/123/789.htm",
"rating": null,
"rank": null,
"view_nums": null,
"praise_nums": null,
"create_date": "2026-10-05",
"content": "(截 300 字)……",
"crawled_at": "2026-10-06 09:30:02"
}
]
}编辑与清理的响应(200):
{"ok": true}{"ok": true, "deleted": 4831}契约要点:管理视角与公开接口的差异——不排除榜单来源(全量数据)、按 crawled_at 倒序、列表 content 截 300 字;PUT 只接受白名单四字段 title/content/tags/author 的子集(其他字段传了直接 400,title/author 变更自动重建 suggest 补全字段);DELETE 同路径同视图(三方法共用,勿拆)。purge 的 confirm 必须逐字等于 source(不一致 400 {"error": "confirm 与 source 不一致,拒绝执行"}),服务端先 refresh 再删、残余补删一轮、成功后联动清新闻缓存(《05-核心流程》流程五)。overview 端点返回 {total, size_bytes, by_source[]}(size 30,来自 indices.stats 主分片计数)。
这一组是全站唯一的"可写"接口面,两条设计取向需要点破。其一,白名单不是过滤而是拒绝:PUT 的字段白名单 DB_DOC_EDITABLE_FIELDS 只取四个允许键,请求体里出现 url、crawled_at、source 这类不可编辑字段不会"被忽略"而是整体 400——测试 test_doc_update_rejects_non_whitelisted_fields 把这个行为钉死,因为它表达的语义是"编辑范围有边界"而不是"编辑范围外的内容无所谓"。其二,危险操作是双层确认:purge 的 confirm 逐字校验在后端,手输来源名的 UI 约束在前端(DbAdminView 要求输入框内容与所选来源一致才启用按钮),两层各自独立成立;配合 ES 侧"先 refresh 再删、删完校验残余补删一轮"的执行序列,兑现"清空该来源"的承诺。误删的最终兜底是每日 ES 快照(deploy/backup.sh,保留 14 天),恢复流程见《06-部署运行》4.8——顺序不能反:先起 ES 灌快照,再覆盖状态文件。
以下是 api_search 在请求 q=人工智能&source=news_ithome&days=30&sort=time 时实际发送到 ES quotes 索引的查询体(对照 api_views.py 的 _run_search/_HIGHLIGHT/_FACET_AGGS/_SORTS,值示意)。读懂这个 DSL 等于读懂搜索接口的全部行为:
{
"query": {
"bool": {
"must": {
"multi_match": {
"query": "人工智能",
"fields": ["title^3", "tags^2", "author", "content"],
"minimum_should_match": "2<80%"
}
},
"must_not": [
{"terms": {"source": ["douyin_hot", "aihot_hot"]}}
]
}
},
"post_filter": {
"bool": {
"must": [
{"term": {"source": "news_ithome"}},
{"range": {"create_date": {"gte": "now-30d"}}}
]
}
},
"from": 0,
"size": 10,
"sort": [
{"create_date": {"order": "desc", "missing": "_last"}},
"_score"
],
"highlight": {
"pre_tags": ["<span class=\"kw\">"],
"post_tags": ["</span>"],
"fields": {"title": {}, "content": {}}
},
"aggs": {
"facet_sources": {"terms": {"field": "source", "size": 20}},
"facet_days": {
"date_range": {
"field": "create_date",
"ranges": [
{"key": "7", "from": "now-7d"},
{"key": "30", "from": "now-30d"},
{"key": "90", "from": "now-90d"}
]
}
}
}
}三层语义再复述一遍:must 的 multi_match 决定相关性与召回(title 三倍权重;超 2 词元要求 80% 命中);must_not 永久排除榜单来源(它们没有发布时间语义,ADR-07);source/days 筛选放 post_filter——聚合桶统计的是 post_filter 之前的命中集,分面计数才能保持"其他筛选条件下"的口径。sort 里 _score 作次序兜底、missing: "_last" 让无日期文档沉底。若首查询 0 结果,还会追加两次补救查询:phrase suggester({"suggest": {"text": "…", "title_phrase": {"phrase": {"field": "title", "size": 1}}}})与模糊重搜(multi_match 加 "fuzziness": "AUTO"、minimum_should_match 放宽为 "2<50%")。
<span class="kw">)是唯一携带 HTML 的内容,渲染前必须整体转义再还原;id(ES _id)是详情与单文档管理的唯一寻址键,列表类接口必带;stats 的 es_ok:false、suggest 的 []——收到 200 仍需检查业务降级字段;code 字段只有三个值(auth_required/not_found/too_many_login_attempts),机器判断读它,文案展示读 error;Retry-After 是秒数;409 是正常业务态;rankings 两种分支响应形状不同,按 source 区分;url 是源站外链;401 检查(token 过期/缺失),token 进程内存储、重启即失效;view_nums 语义随 source 变化(热度值/信源数),不要假设同一含义;