项目:nas-console | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
本篇是「可以直接照着敲」的接口示例集:server 的核心端点每个都给出完整的请求与响应 JSON、错误分支示例与可复制的 curl 命令,SSE 给出原始帧流。与 15-接口全量参考的分工是:15 篇按端点列表组织,回答「有哪些接口、各自入参出参是什么、被谁消费」;本篇按数据契约组织,回答「一次真实的调用长什么样、字段在 JSON 里的确切拼写是什么、出错时错误体会说什么」。两者内容互补而不重复——本篇不再罗列消费方与鉴权面划分(见 15 篇第 1 节端点总表),只在字段级细节处回引。
三条使用约定必须先声明。其一,示意值标注:所有 JSON 中的时间戳、延迟、日期、URL、容器名、统计数值均为依据源码结构构造的示意值(本文标注为「示意」),字段名、类型、枚举值、嵌套结构与 server/src/server.ts / server/src/types.ts 逐一核对过,可放心按字段名消费;本文不含任何真实凭据——口令一律写作占位符,apiKey 一律写作占位符,会话 Cookie 的 payload/sign 用截断示意。其二,示例环境:curl 以本地 dev 探针 http://127.0.0.1:5951 为基址(vite 代理目标,06 篇第 2 节);生产容器内是 5950。启用 auth 后受保护端点需要 Cookie,下文统一用 -c(写入)与 -b(携带)两个参数管理 cookie jar。其三,响应头约定:全部 JSON 响应带 content-type: application/json; charset=utf-8 与 cache-control: no-store(server.ts 的 json() 函数),下文不再重复;唯一例外是封面图片(max-age=86400)与静态 assets(immutable)。
POST /api/auth/login(公开)请求体两个字段,均为字符串(auth.login 的第三、四参数):
POST /api/auth/login HTTP/1.1
content-type: application/json
{ "username": "admin", "password": "<你的口令>" }成功响应(200)的 body 只有一个键,会话在响应头里:
HTTP/1.1 200 OK
set-cookie: nas-console-session=eyJ1IjoiYWRtaW4iLCJleHAiOjE3NjIyNDM2MDB9.<HMAC-SHA256签名,示意截断>; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000
content-type: application/json; charset=utf-8
{ "ok": true }Cookie 值是 <payload>.<signature> 两段:payload 是 {"u":"admin","exp":<毫秒时间戳>} 的 base64url(示意值中 exp 为 30 天后,SESSION_TTL_MS = 30 * 24 * 3600_000),签名是对 payload 原文的 HMAC-SHA256(base64url,auth.ts 第 92 行)。属性串固定四项:Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000——注意没有 Secure(08 篇第 5 节有边界分析)。
失败响应统一 401,error 文案按触发源分四种,逐一列出(这是排障时区分「哪一层在拦」的依据):
{"error":"用户名或密码错误"}{"error":"失败次数过多,锁定 10 分钟"}(第 5 次失败时返回,MAX_FAILS=5、LOCKOUT_MS=10min){"error":"尝试过于频繁,请 7 分钟后再试"}(N 为剩余分钟向上取整,示意值){"error":"未启用登录(config.json 未配置 auth)"}curl 复现(启用 auth 的环境):
# 登录并把会话存进 cookie jar
curl -s -c /tmp/nas-cookie.txt -X POST http://127.0.0.1:5951/api/auth/login \
-H 'content-type: application/json' \
-d '{"username":"admin","password":"<你的口令>"}'
# => {"ok":true}
# 之后所有受保护端点都带 -b /tmp/nas-cookie.txtGET /api/auth/session(公开)与登出 POST /api/auth/logout(需会话){ "authRequired": true, "authed": false }authRequired 即 auth.enabled(config.json 的 auth 段三条件:非空、username 长度大于 0、passwordHash 含 $);authed 即 verify(req) 结果。前端 RequireAuth 挂载时调它一次:两者组合决定渲染登录页还是放行。登出端点注意一个契约细节:它不在公开白名单里,未登录调用会得到 401 {"error":"unauthorized"}——登出本身需要有效会话。成功时返回 {"ok":true} 且 set-cookie: nas-console-session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0。它只清浏览器侧 Cookie,服务端无任何状态变更(无会话黑名单,08 篇第 2.2 节)。
GET /api/health/latest(需会话){
"ts": 1759741200000,
"results": [
{ "id": "admin-vue", "status": "up", "latencyMs": 87, "checkedAt": 1759741195000, "certExpiresInDays": 62 },
{ "id": "x-pan", "status": "up", "latencyMs": 122, "checkedAt": 1759741193000 },
{ "id": "dsm", "status": "down", "latencyMs": null, "checkedAt": 1759741191000, "error": "HTTP 401" }
]
}(时间戳与延迟为示意值。)契约要点:results 只包含已纳入巡检的服务——check.type: "none" 与 hidden: true 的服务不出现;status 枚举仅 up/down 两个值(没有「unknown」,未检过 = 不在数组里);latencyMs 在 down 时恒为 null;error 仅 down 时存在;certExpiresInDays 仅 HTTPS 检查成功时存在且不入库——进程重启后到下一轮 HTTPS 检查之前该字段缺失,消费方必须判空(types.ts 第 100 行注释「仅内存快照,不入库」)。
GET /api/health/history?days=90(需会话){
"days": {
"admin-vue": {
"2026-10-05": { "up": 2878, "down": 2, "latencySum": 264376, "latencyN": 2878 },
"2026-10-06": { "up": 1440, "down": 0, "latencySum": 131040, "latencyN": 1440 }
}
},
"recent": {
"admin-vue": [
{ "t": 1759730400000, "s": 1, "l": 91 },
{ "t": 1759730430000, "s": 1, "l": 88 }
]
}
}两个顶层键的窗口不对称是设计使然:days 受 ?days= 控制(clamp 到 1–90,缺省 45,前端固定传 90),recent 恒为最近 48 小时(server.ts 第 269 行硬编码 48 * 3600_000)。days 的值是 DayBucket 四字段——可用率 = up/(up+down)*100(0–100 百分数,不是 0–1 小数),日均延迟 = latencySum/latencyN(仅 up 采样计入,down 的 latencyN 贡献为 0,见 18 篇 2.2 节)。recent 的 RecentSample 用短键 t/s/l(毫秒时间戳 / 1=up / 延迟 ms),每服务降采样至 ≤240 点。某服务无数据时,两个映射里没有该服务的键(而非空对象占位)。
GET /api/events?limit=100(需会话)——注意线格式的蛇形键{
"events": [
{ "service_id": "admin-vue", "ts": 1759741195000, "to": "up" },
{ "service_id": "dsm", "ts": 1759730400000, "to": "down" }
]
}这是全 API 中唯一线格式与 TS 类型不一致的端点:store.events() 把 SQL 行 as ProbeEvent[] 强转,但 better-sqlite3 返回的键是列名——service_id(蛇形)与 to(AS "to" 别名),并非 serviceId。前端的 fetchProbeEvents(src/api/probe.ts 第 61-66 行)按真实形状读取并映射为驼峰 DTO。第三方消费方务必按本例的蛇形键解析,不要照抄 types.ts 的 interface。limit clamp 到 1–200(缺省 50,前端传 100),返回按 ts 全局倒序(跨服务混排)。
对照项:SSE 的 event 帧(第 4 节)承载同类数据却是驼峰 serviceId——因为那条路径的事件是 store.record() 在 JS 里构造的对象({ serviceId, ts, to }),不经 SQL 行。同一份数据走两条通道,键风格不同,这是读 15 篇端点表时容易忽略的坑。
GET | PUT /api/services(需会话){
"services": [
{
"id": "admin-vue",
"name": "admin-vue 后台管理系统",
"description": "NestJS 微服务 + pure-admin 前台",
"group": "应用站点",
"url": "https://admin-vue.example.com",
"internalPort": 5800,
"icon": "shield",
"tags": ["React", "NestJS"],
"checkable": true,
"hidden": false,
"check": { "type": "http", "intervalSec": 30, "keyword": "登录", "tlsSkipVerify": false },
"profile": { "cover": "/portal-covers/admin-vue.png", "story": "<示意>", "highlights": ["<示意>"] }
},
{
"id": "mysql",
"name": "MySQL",
"group": "数据库与中间件",
"url": "",
"internalPort": 3306,
"checkable": true,
"check": { "type": "tcp" }
}
]
}check 段的完整字段集见 types.ts 第 5-18 行:type(http/tcp/none)、可选 intervalSec/expectStatus/keyword/tlsSkipVerify/tcpHost/tcpPort。第二个条目演示了纯内网 TCP 服务(url 为空串 + tcp 检查)——它不会出现在公开快照的白名单展开里,也解释了为什么 defaultCheck 的推导规则是「checkable 且有 url 才是 http」。
请求体是「内置 + 自定义 − 隐藏标记」的展平数组,每项的 check 由前端按 checkable/url 推导(syncProbeServices,src/api/probe.ts 第 106-127 行):
PUT /api/services HTTP/1.1
content-type: application/json
{
"services": [
{
"id": "admin-vue", "name": "admin-vue 后台管理系统",
"description": "NestJS 微服务 + pure-admin 前台", "group": "应用站点",
"url": "https://admin-vue.example.com", "internalPort": 5800,
"icon": "shield", "checkable": true, "hidden": false,
"check": { "type": "http" }
},
{
"id": "custom-lz3k9x", "name": "我的小工具", "group": "工具",
"url": "https://tools.example.com", "checkable": true, "hidden": false,
"check": { "type": "none" }
}
]
}(custom- 前缀 id 是前端 custom-${Date.now().toString(36)} 生成的形态;description/internalPort/icon 等可省略,undefined 字段不进 JSON。)成功响应:{"ok": true, "total": 19}(total 为合并后清单总长,示意值)。合并语义复习(详见 config.ts syncServices 与 03 篇第 6.2 节):已存在 id 的 check 以探针侧为准(上例 admin-vue 若在探针 config.json 里手工配了 keyword,传入的 {type:'http'} 被忽略);新 id 用传入 check 或推导默认值;profile 必经 sanitizeProfile 净化(类型不符的字段静默丢弃);prune 固定 false——探针有而清单没有的服务原样保留尾部;只重写 services 段,alerts/auth/portal/onePanel 原样保留。错误响应两种:{"error":"services 应为数组"} 与 {"error":"服务缺少 id/name"},均 400。
一个值得写明的推论:PUT 传入的 hidden: true 服务仍会被落盘(不删除),但 activeServices 会把它排除出巡检与公开快照——「隐藏≠删除」是贯穿清单模型的不变量。
GET /sse(需会话)以下是 curl -N -H "Cookie: nas-console-session=<...>" http://127.0.0.1:5951/sse 看到的原始字节流(内容示意,空白行是帧边界):
: connected
event: status
data: {"id":"admin-vue","status":"up","latencyMs":87,"checkedAt":1759741195000,"certExpiresInDays":62}
event: status
data: {"id":"dsm","status":"down","latencyMs":null,"checkedAt":1759741191000,"error":"超过 8s 未响应"}
event: batch
data: [{"id":"admin-vue","status":"up","latencyMs":87,"checkedAt":1759741195000,"certExpiresInDays":62},{"id":"dsm","status":"down","latencyMs":null,"checkedAt":1759741191000,"error":"超过 8s 未响应"}]
event: event
data: {"serviceId":"dsm","ts":1759741191000,"to":"down"}
: ping
帧语法逐项说明:注释行以冒号开头(: connected、: ping),浏览器 EventSource 不派发事件,作用是让链路字节流动(connected 立即写、ping 每 25 秒);命名事件由 event: 行声明,data: 行是单行 JSON(不换行的序列化结果),空行结束一帧。三类事件的载荷:status = 单个 CheckResult(驼峰,与 /api/health/latest 的成员完全同形);batch = CheckResult 数组全量(30 秒一拍,合并语义是按键整体覆盖);event = {serviceId, ts, to}(注意驼峰,与 2.3 节 /api/events 的蛇形线格式形成对照)。响应头四件套:content-type: text/event-stream; charset=utf-8、cache-control: no-store、connection: keep-alive、x-accel-buffering: no。断线重连由浏览器 EventSource 内建,服务端无会话级握手参数;event 事件当前前端未消费(15 篇第 2 节已注明,保留给扩展)。
GET /api/host/metrics(需会话){
"ts": 1759741200000,
"current": {
"load1": 0.52, "load5": 0.48, "load15": 0.45, "loadUsagePercent": 12.6,
"cpuUsedPercent": 23.4, "cpuTotal": 8,
"memoryTotal": 16642998272, "memoryUsed": 9123844096, "memoryUsedPercent": 54.8,
"swapMemoryTotal": 2147483648, "swapMemoryUsedPercent": 3.1
},
"os": {
"os": "linux", "platform": "debian", "kernelArch": "x86_64", "kernelVersion": "5.15.0+",
"prettyDistro": "Debian GNU/Linux 12 (bookworm)", "diskSize": 429496729600
}
}(数值全部为示意;字段结构对应 PanelNodeCurrent/PanelOsInfo,1panel.ts 第 20-42 行。)单位契约:memoryTotal/memoryUsed 等内存字段上游为 KB(前端 fmtBytes 连除两次 1024 的换算前提),diskSize 为字节;cpuTotal 是核数。错误两档:400 {"error":"未配置 1Panel(config.json 的 onePanel 段)"}——前端据此置 notConfigured 并整卡隐藏;502 {"error":"1Panel 暂不可用"}——固定文案,不透传 panelMsg(内含面板 host:port,server.ts 第 143 行注释),这是本端点与 containers 端点在错误脱敏上的关键差异。
GET /api/containers?name= 与 POST /api/containers/operate(需会话){
"containers": [
{
"containerID": "a1b2c3d4e5f6",
"name": "nas-console-probe",
"state": "running",
"imageName": "nas-console-probe-probe:latest",
"runTime": "3 days ago",
"ports": ["5950/tcp"],
"appName": "nas-console"
}
]
}?name= 传模糊搜索关键词(可空 = 全量,面板侧限 100 条,searchContainers 的固定 body)。操作端点请求体 {"names": ["nas-console-probe"], "operation": "restart"},成功 {"ok": true}。动作白名单是服务端把守的:start/stop/restart/kill/pause/unpause 六个,remove 等破坏性操作返回 400 {"error":"不允许的操作:remove"}(第 243 行注释「remove 等破坏性操作不放行」);缺 names/operation 返回 400 {"error":"缺少 names/operation"}。本组端点的 502 错误携带 panelMsg(如 {"error":"1Panel 密钥或 IP 白名单校验失败"} 或 {"error":"1Panel 请求超时(8s)"})——它们在鉴权面内,可透传语义化文案,与 host/metrics 的固定文案形成有意的不对称。
GET /api/public/status(公开,无需会话)完整响应骨架(字段存在性即脱敏语义,值示意):
{
"ts": 1759741200000,
"portal": { "publicMetrics": false },
"groups": [
{
"name": "应用站点",
"services": [
{
"id": "admin-vue", "name": "admin-vue 后台管理系统",
"description": "NestJS 微服务 + pure-admin 前台", "group": "应用站点",
"icon": "shield",
"status": "up", "latencyMs": 87, "checkedAt": 1759741195000,
"certExpiresInDays": 62,
"days": [
{ "date": "2026-08-23", "availability": 99.93 },
{ "date": "2026-08-24", "availability": 100.0 },
{ "date": "2026-08-25", "availability": null }
],
"url": "https://admin-vue.example.com",
"tags": ["React", "NestJS"],
"profile": {
"cover": "/portal-covers/admin-vue.png",
"story": "<站长撰写的项目故事,示意>",
"highlights": ["<亮点一>", "<亮点二>"],
"timeline": [{ "date": "2025.03", "text": "<时间线条目>" }],
"role": "独立开发", "repo": "https://github.com/<owner>/<repo>",
"screenshots": ["/portal-covers/admin-vue-s1.png"]
}
}
]
},
{
"name": "面板与运维",
"services": [
{
"id": "1panel", "name": "1Panel", "group": "面板与运维", "icon": "server",
"status": "up", "latencyMs": 12, "checkedAt": 1759741195000,
"days": [{ "date": "2026-08-23", "availability": 100.0 }]
}
]
}
]
}对照两个分组即可读出全部脱敏规则:基础字段(id/name/description/group/icon/status/latencyMs/checkedAt/certExpiresInDays/days)所有分组都有;url/tags/error/profile 四个字段只有白名单分组(portal.publicGroups,缺省「应用站点/文档与统计」,字符串精确匹配)才出现——第二组的 1Panel 条目没有这四个键,连 error 都不下发(ECONNREFUSED 类错误可能带出内网 host:port)。days 恒为最近 45 天数组(含今天,升序),无数据日 availability: null;status/latencyMs/checkedAt 在服务尚未被检过时为 null。profile 的七个字段全部可选且必经 sanitizeProfile 净化——类型不符的字段不会出现在响应里。
GET /api/public/metrics(公开)与封面托管{ "ts": 1759741200000, "current": { "cpuUsedPercent": 23.4, "memoryUsedPercent": 54.8, "loadUsagePercent": 12.6, "cpuTotal": 8 } }恒定只有这四个字段——publicMetrics: false 或未配置面板时返回 404 {"error":"not found"}(不是 403:不向访客暴露「端点存在但被关闭」);面板故障时 502 且 error 携带 panelMsg(已回源核实:server.ts 第 212 行直接使用 panelMsg(error),本端点是「公开语义」之外的遗留透传点,与 host/metrics 的固定文案不同——01 篇决策五与 5.2 节已按此口径修订,是本组端点中最值得在加固时统一的一处)。封面托管 GET /portal-covers/<file>:文件名必须匹配 ^[a-z0-9-]+\.(png|jpe?g|webp)$,命中返回图片流(cache-control: public, max-age=86400),否则 404(无响应体)。
POST /api/alerts/test(需会话)——恒 200,渠道成败在 results 里逐项携带:
{
"results": [
{ "channel": "bark", "ok": true },
{ "channel": "telegram", "ok": false, "error": "HTTP 401" },
{ "channel": "webhook", "ok": true }
]
}(未配置的渠道不出现在数组里;示例的第二项为示意。)POST /api/portal/upload?service=admin-vue(需会话)——body 是原始图片字节,Content-Type 决定扩展名(png/jpeg/webp 三种映射到 .png/.jpg/.webp),≤5MB;成功 {"url": "/portal-covers/admin-vue.png"}。错误四档(均 400):service id 不合法(id 不匹配 ^[a-z0-9-]+$)/ 仅支持 png/jpeg/webp / 空文件 / 文件超过 5MB 限制。注意上传与档案保存是两个动作——上传只落文件,profile.cover 引用它需要再走一次 PUT /api/services(孤儿文件不自动清理,04 篇第 1.5 节)。
GET /statusz(公开,容器 healthcheck 用):
{ "ok": true, "services": 19, "sseClients": 2, "authRequired": true, "ts": 1759741200000 }(services 为 config 清单总条数含 hidden/none,sseClients 为当前 SSE 连接数。)最后是两条兜底契约:未匹配任何 API 分支的 /api/* 返回 404 {"error":"not found"};处理器大 try/catch 兜住的未捕获异常返回 500 {"error":"<异常消息>"}。dev 代理陷阱要在这里重申一次:本地 dev 若 vite 代理漏配新路由前缀,API 请求会命中 SPA fallback 返回 200 + text/html 的 index.html——表现为「请求成功但 JSON 解析炸」,排查第一动作是看 content-type(15 篇第 1.7 节)。
| 状态码 | 语义 | error 文案来源 | 出现端点 |
|---|---|---|---|
| 400 | 入参错 / 未配置 | 服务端固定中文文案 | PUT services、upload、containers/operate、host/* 未配置面板 |
| 401 | 未登录 / 凭据错 / 限流 | unauthorized(守卫)或具体中文原因(登录) | 全部需会话端点 |
| 404 | 未匹配 / 未开放 | not found | API 兜底、public/metrics 关闭时(故意) |
| 500 | 未捕获异常 | 异常 message | 兜底 |
| 502 | 上游 1Panel 失败 | 1Panel 暂不可用(host/metrics)或 panelMsg(containers 组、public/metrics) | 全部面板代理端点 |
把全篇示例并排回看,五条契约铁律可作为消费方(包括未来第三方客户端)的编码依据。第一,成功读字段、失败读 error:没有统一包络,写端点成功必有 ok: true,查询端点直接返回领域对象。第二,时间一律毫秒:ts/checkedAt/t/exp 全部 Unix 毫秒,唯一例外是 1Panel 出站签名用秒(其 API 规范)。第三,字段存在性即语义:公开快照的 url 决定能否成卡,certExpiresInDays 决定是否渲染证书 Tag,error 的有无决定 Tooltip——判 != null 而非判真值。第四,列表参数被静默夹取:?days=(1–90)与 ?limit=(1–200)越界不报错,回落合法值。第五,线格式以本文为准:/api/events 的 service_id 蛇形、SSE event 帧的 serviceId 驼峰、RecentSample 的 t/s/l 短键,三处非直觉拼写均已从源码验证——接手者改动这些端点时,本篇示例就是回归基线。
端点全集与消费方映射见 15-接口全量参考第 1 节;字段类型的权威定义见 server/src/types.ts 与 04-数据模型;鉴权流程的端到端时序见 05-核心流程第 3 节;SSE 三层容错的设计动机见 01 篇第 5.1 节与 18 篇第 3 节。