项目:webCommand | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
《15-接口全量参考》已给出全部端点的参数表与语义表,本篇做 15 号文档刻意不做的事:为 10 个核心接口构造完整可复制的请求/响应报文示例(含成功、典型失败、curl 三件套),让读者可以对着报文理解契约,甚至直接在本地 pnpm dev 后逐条回放验证。所有 JSON 均按源码 schema 构造——响应包络取自 server/utils/response.ts 的 ok/fail,用户对象取自 server/models/User.ts 的 toSafeUser(password 剥离),错误码取自 ERROR_CODE 常量。凡是为了示例而编造的值(用户名、id、时间戳、blob 内容)一律标注「示意」;结构性的字段名与取值域则不标,因为它们是源码事实。
四条通用约定先立在这里,后续各节不再重复。其一,包络:所有业务接口统一返回 {code, data, message},code: 0 成功(此时无 message 字段),非 0 失败(fail() 里 data: null);业务错误默认 HTTP 200,仅当 BizError 显式携带 statusCode 时才改写 HTTP 状态(限流 429、天气代理上游失败 502)。其二,错误码域:40000 参数错误、40100 未登录、40300 禁止、40400 不存在、42900 限流、50000 系统错误、50010 第三方失败(server/utils/response.ts)。其三,会话:登录/注册成功后由 nuxt-auth-utils 写入名为 nuxt-session 的 HttpOnly 加密 cookie;后续请求凭 cookie 鉴权,服务端 userId 一律取自会话(《01》§1 第三认知)。其四,限流的现行为:auth 组五个路由(register/login/reset-code/reset-password/delete)命中限流时抛的是 ERROR_CODE.PARAMS(body 40000)+ HTTP 429,而 data/proxy 组抛 ERROR_CODE.RATE_LIMIT(body 42900)——两组不一致是《13》⚠️-5 已登记的债务,本篇示例如实按当前源码行为书写,若未来统一了限流码,请以新行为为准并同步本篇。以下示例的基地址统一为 http://localhost:3000(pnpm dev 默认端口)。
注册是唯一"无会话即可写库"的接口。准入链路:authLimiter.hit(event)(每 IP 每分钟 10 次,超限 HTTP 429 + body 40000)→ 读 body → userRegister()(server/services/userService.ts)内依次做非空、用户名 ≤32、口令 ≥8(accountPolicy.validateNewPassword)、邮箱正则校验,然后查重(Op.or 同 username 或 email)、bcrypt 10 轮哈希后建行,唯一索引冲突兜底转译为同一句业务提示。
请求:
{
"username": "demo",
"password": "demo-pass-2026",
"email": "demo@example.com"
}成功响应(data 为新用户 id,数字类型):
{
"code": 0,
"data": 42
}典型失败(口令过短;查重命中也是同一路径,提示语不同):
{
"code": 40000,
"message": "密码至少 8 位",
"data": null
}curl -s -X POST http://localhost:3000/api/user/register \
-H 'Content-Type: application/json' \
-d '{"username":"demo","password":"demo-pass-2026","email":"demo@example.com"}'注意两点契约细节:查重命中返回的是 40000「该用户名或邮箱已被注册」而非 40400/40900——错误码体系没有"冲突"档,注册与登录的报错刻意不做账号枚举区分(登录侧见第 3 节);注销后的用户名仍会被查重命中(软删行持有唯一索引,userRegister 的查重无 isDelete 过滤),这是《13》⚠️-3 的语义,报文层面表现为"注销后的用户名再注册会得到已被注册"。
准入链路:authLimiter → verifyCredentials()(按 username 查行 → isDelete 过滤 → status 封禁判定 → bcrypt 比对,"用户不存在"与"密码错误"统一报 40400 同一句话)→ setUserSession 写 cookie。remember 字段可选:缺省或非 false 均按"自动登录"持久化 30 天(SESSION_30D = 60*60*24*30),显式 false 则用会话默认策略(浏览器会话 cookie)。
请求:
{
"username": "demo",
"password": "demo-pass-2026",
"remember": true
}成功响应(data 为脱敏 SafeUser;createTime/updateTime 因模型 timestamps: false 且无写路径恒为 null,status/isDelete 为 0 正常值):
{
"code": 0,
"data": {
"id": 42,
"username": "demo",
"email": "demo@example.com",
"status": 0,
"createTime": null,
"updateTime": null,
"isDelete": 0
}
}响应同时携带 Set-Cookie:nuxt-session=…(示意),HttpOnly,remember: true 时 Max-Age 为 2592000。
失败响应三例(账号密码错 / 封禁 / 限流):
{
"code": 40400,
"message": "用户不存在或密码错误",
"data": null
}{
"code": 40100,
"message": "账号已被封禁",
"data": null
}{
"code": 40000,
"message": "操作过于频繁,请稍后再试",
"data": null
}第三例 HTTP 状态为 429(auth 组限流现行为 body 40000,见第 1 节其四)。curl(用 cookie jar 承接会话,供后续接口复用):
curl -s -c cookies.txt -X POST http://localhost:3000/api/user/login \
-H 'Content-Type: application/json' \
-d '{"username":"demo","password":"demo-pass-2026"}'「用户不存在或密码错误」的三重含义值得展开:not found、软删行、密码不匹配三种内部状态对外完全同形,这是《08》§3 的防枚举设计;但也意味着用户无法区分"账号真不存在"与"密码敲错",找回密码(第 6 节)因此承担了恢复路径的职责。
无 body。链路:读会话 → session.user?.id 缺失即 40100 → getLoginUserById 从数据库重读(不是把会话里的旧值直接返回——改密、改名等未来变更在下次请求即生效)→ 脱敏返回。它是前端启动时 userStore.getAndSetLoginUser() 的对账接口(《08》§2 的"展示态与真相分离")。
成功响应(结构同第 3 节 data):
{
"code": 0,
"data": {
"id": 42,
"username": "demo",
"email": "demo@example.com",
"status": 0,
"createTime": null,
"updateTime": null,
"isDelete": 0
}
}失败响应(未登录——前端把 40100 视为"静默保持 LOCAL_USER 默认态"的预期路径,见 userStore.ts 注释):
{
"code": 40100,
"message": "未登录",
"data": null
}curl -s -b cookies.txt -X POST http://localhost:3000/api/user/current无 body,无限流(唯一不带 limiter 的 user 组路由,server/api/user/logout.post.ts 全文仅 7 行)。链路:读会话确认已登录(否则 40100)→ clearUserSession(event) 清 cookie → 返回 true。幂等性注意:会话已失效时重复调用会得到 40100 而非 true——严格说它不是幂等接口,前端 logoutCommand 对此不重试。
{
"code": 0,
"data": true
}curl -s -b cookies.txt -c cookies.txt -X POST http://localhost:3000/api/user/logout准入链路:authLimiter → 非空校验 → isMailConfigured()(SMTP 未配置即 50010,提示联系管理员)→ findActiveUserByEmail(含 isDelete: 0 过滤)→ 未注册邮箱也返回 true(防枚举,源码注释原文"不泄露邮箱是否已注册")→ issueCode(内存验证码:6 位数字、sha256 哈希存储、10 分钟 TTL、5 次尝试上限、60 秒重发间隔,重发间隔命中抛 40000 带 waitSec 提示)→ nodemailer 发信。
请求:
{
"email": "demo@example.com"
}成功响应(无论邮箱是否注册):
{
"code": 0,
"data": true
}失败两例(重发过频 / 邮件服务不可用):
{
"code": 40000,
"message": "发送过于频繁,请 43 秒后再试",
"data": null
}{
"code": 50010,
"message": "邮件发送失败,请稍后再试",
"data": null
}curl -s -X POST http://localhost:3000/api/user/reset-code \
-H 'Content-Type: application/json' \
-d '{"email":"demo@example.com"}'「43 秒」是示意值——它是 RESEND_INTERVAL(60s) - 距上次发送耗时 的实时计算结果,每次请求不同。验证码的完整安全参数(TTL/尝试上限/哈希存储)见《08》§4。
准入链路:authLimiter → 三字段非空 → verifyCode(成功即失效——验证码一次性;超时/超次/不存在一律返回"错误或已过期")→ findActiveUserByEmail → resetUserPassword(不校验旧密码,这是它与 user passwd 的本质区别)→ bcrypt 重哈希写库。
请求(newPassword 仍受 ≥8 位校验,assertNewPassword 在 resetUserPassword 内):
{
"email": "demo@example.com",
"code": "042517",
"newPassword": "demo-pass-2027"
}成功响应:
{
"code": 0,
"data": true
}失败两例(验证码不对 / 新口令不达标):
{
"code": 40000,
"message": "验证码错误或已过期",
"data": null
}{
"code": 40000,
"message": "密码至少 8 位",
"data": null
}curl -s -X POST http://localhost:3000/api/user/reset-password \
-H 'Content-Type: application/json' \
-d '{"email":"demo@example.com","code":"042517","newPassword":"demo-pass-2027"}'code 是字符串不是数字——issueCode 用 padStart(6,'0') 保留前导零,若前端按数字传会把 042517 变成 42517 而校验失败,联调时的高频踩坑点(《12》§2 速查表收录过同类问题)。
全站约束最严的接口。准入链路:authLimiter → 会话 → checkAccountDeletion 三条件(已登录;confirm 与用户名完全一致;password 非空)→ deleteAccount(bcrypt 校验口令 → 事务:user_data 物理删除 + user 软删 isDelete=1 → commit)→ clearUserSession。三条件的设计动机写在 accountPolicy.ts 注释里:确认串让"删哪个账号"是明确表达,重输口令防止会话被借用时凭会话删号。
请求(confirm 必须等于当前登录用户名,「demo」为示意):
{
"password": "demo-pass-2027",
"confirm": "demo"
}成功响应(cookie 同时被清除):
{
"code": 0,
"data": true
}失败两例(确认串不符 / 口令错误):
{
"code": 40000,
"message": "确认串必须与用户名完全一致,请执行 user delete --confirm demo",
"data": null
}{
"code": 40000,
"message": "密码错误",
"data": null
}curl -s -b cookies.txt -X POST http://localhost:3000/api/user/delete \
-H 'Content-Type: application/json' \
-d '{"password":"demo-pass-2027","confirm":"demo"}'错误消息里直接给出可执行的修正命令(user delete --confirm demo),这是"错误消息即操作指引"的样本。注销语义的两个后果在此复述一遍防误用:云端六类数据物理删除不可恢复;用户名与邮箱因软删行占用唯一索引而永久不可重新注册(《13》⚠️-3)。
这两个接口是云同步协议的服务端半边,准入链路相同且都走 dataLimiter(每 IP 每分钟 120 次,配额按"每次同步 = 拉取 + 上传两个请求"翻倍设计)。读:checkDataRead(会话 userId + type 白名单六类之一)→ findOne → 返回 content 字符串或 null("从未同步过"与"同步了空串"在协议层同为 null,《04》§6.3)。写:checkDataWrite 追加 content 类型与尺寸校验(数据类 1MB、配置类 64KB,Buffer.byteLength 计 utf8 字节)→ upsert((userId, type) 唯一键冲突即更新)。
读请求示例(type 取值域:todo / space / note / custom / terminalConfig / theme):
curl -s -b cookies.txt 'http://localhost:3000/api/data?type=todo'读成功响应(content 为整包 blob 字符串,内部结构由前端定义;「示意」值为 todo 的 items+deleted 双段结构):
{
"code": 0,
"data": "{\"items\":[{\"id\":\"k3x9q2\",\"name\":\"学一下 help 里的命令\",\"isFinished\":false,\"createTime\":\"2026-10-06T03:15:00.000Z\",\"updateTime\":1760012100000}],\"deleted\":{\"a1b2c3\":1760010000000}}"
}未同步过时 data 为 null:
{
"code": 0,
"data": null
}写请求(content 必须是字符串化的 JSON,不是内嵌对象——checkDataWrite 校验 typeof content !== 'string' 即拒):
curl -s -b cookies.txt -X POST http://localhost:3000/api/data/sync \
-H 'Content-Type: application/json' \
-d '{"type":"todo","content":"{\"items\":[],\"deleted\":{}}"}'写成功响应:
{
"code": 0,
"data": true
}写失败三例(未登录 / type 不合法 / 超尺寸):
{
"code": 40100,
"message": "未登录",
"data": null
}{
"code": 40000,
"message": "type 不合法",
"data": null
}{
"code": 40000,
"message": "content 超过 1048576 字节上限(本次 1048600 字节)",
"data": null
}限流命中(data 组是 42900,与 auth 组不同):
{
"code": 42900,
"message": "同步过于频繁,请稍后再试",
"data": null
}伪造 userId 的契约值得单独点名:请求体里没有 userId 字段,写了也被忽略——userId 只来自会话。test/routes/dataRoutes.test.ts 有专门用例钉住"伪造 userId 无效",这是《01》§1"服务端裁决"原则在报文层的体现。blob 内容里的 updateTime 为毫秒数(LWW 时钟)、deleted 为 id→删除时刻的墓碑表(30 天窗口),协议细节见《05》§4 与《16》ADR-006。
运维链路的契约,无鉴权、无限流。inspectDb 每次现查 SELECT 1 + information_schema 两表存在性(《16》ADR-012:不读内存标志),健康返回 200/code:0,不健康返回 503/code:50000。容器 healthcheck 与 deploy.sh verify 消费的就是这个语义。
健康响应(time 为示意值):
{
"code": 0,
"data": {
"status": "ok",
"service": "webcommand-nuxt",
"time": "2026-10-06T12:00:00.000Z",
"dbReachable": true,
"schemaReady": true
}
}降级响应(迁移失败时 schemaError 携带原文供排障;HTTP 状态 503):
{
"code": 50000,
"data": {
"status": "degraded",
"service": "webcommand-nuxt",
"time": "2026-10-06T12:00:00.000Z",
"dbReachable": true,
"schemaReady": false,
"missingTables": ["user"],
"schemaError": "Table 'user' already exists(示意)"
}
}curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/healthmissingTables 与 schemaError 在健康时不存在于响应中(undefined 字段被 JSON 序列化省略),消费方判断健康只看 HTTP 状态或 data.status,不要依赖错误字段缺席与否。
在进入回放清单之前,值得把"这些报文最终变成什么"补完——报文契约的下游是前端的错误消费链,两者合起来才是完整契约。前端侧的规整器是 app/utils/myAxios.ts 的响应拦截器:HTTP 2xx 时直接返回 response.data(即包络本体,code 字段由各命令自行判断);HTTP 401 时统一拒绝为 {code: 40100, message: '未登录'};其他非 2xx 把 error.response.data(也就是服务端 fail() 的产物)原样透传;网络层异常(无响应)规整为 {code: 50000, message: '网络异常'}。因此任何一端抛出的错误在前端眼里都是同构对象,命令实现只需 errMsg(e) 取文案、terminal.writeTextErrorResult 上屏,无需关心错误来自限流、参数还是第三方(这条"错误路径与成功路径同构"的设计在《01》§4 有完整叙述)。以登录为例:报文层的 40400「用户不存在或密码错误」与 HTTP 429 的 40000「操作过于频繁」到达终端时,都只是红字一行——报文的精细区分是给程序与运维看的,终端只呈现人话,这正是服务端精心设计错误码、前端却很少读它的原因:错误码的消费方是测试断言、监控规则与未来的管理面,而不是终端用户。
把上述 10 个接口串成一条可回放的验收链,即是本篇的使用方式。冒烟回放顺序:register(拿 id)→ login(拿 cookie)→ current(对账)→ data/sync 写 todo → data 读回比对逐字一致(含 4 字节 emoji 的往返,scripts/smoke-cloud-sync.sh 第 3 节用例)→ data/sync 写非法 type(应 40000)→ reset-code → reset-password(换新口令)→ 用新口令重登 → delete(注销)→ current(应 40100)。契约维护锚点:改任何接口的字段或错误码时,需同步四处——《15》的参数表、本篇的示例报文、对应路由测试(test/routes/*.test.ts)、以及前端消费点(app/utils/myAxios.ts 的 ApiResult 与各命令的错误分支);限流码若执行《13》⚠️-5 的统一(40000→42900),第 3 节第三例与本节末例要一起改。两点重申:其一,本篇所有报文的字段结构均以源码为准,示意值仅填充内容;其二,auth 组限流 40000 与 data/proxy 组 42900 的不一致是当前事实而非本篇笔误,引用本篇报文做监控或测试断言时,请以"当前源码行为"为准并在债务 ⚠️-5 偿还后更新——文档与代码漂移的教训(《13》⚠️-18)已经证明,示例集的价值一半在准确,一半在标注"何时会过期"。