fix(gateway)+docs(api): F5 区分「字段缺失/值为空白」+ E4/E5 契约文档治理
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- F5:PATCH /sessions/{id} 的 400 文案区分两类——都缺 → at least one of title or pinned is required;
  存在但空白 → title must not be blank(此前空白也回 title is required,误导排查方向)。
  「空串不能清空标题」的既有语义保持不变(用例显式钉住),新增 history_write_test.go 4 段断言
- E4:/auth/logout 的两种真实响应形状(单撤 {revoked} / 全撤 {revoked,revokedCount})+ 错误码;
  /auth/refresh 补 role;登录响应与一次性接入段补 refreshToken(两份文档一致)
- E5:api-reference §3.2 补 pinned/unreadCount/lastMessage/personaId 与准确排序规则
  (pinned DESC, (type='main') DESC, created_at ASC ⇒ 置顶的次级会话可排在主会话之前);
  新增 §3.6a PATCH /sessions/{id}(请求体/错误码/部分更新/title 不可清空)与 §3.6b POST /sessions/{id}/read;
  端点表补 read 行
- api-client 删掉「主会话恒在前」的错误断言(两处),PATCH 错误码文案同步
- DevDocs/02-development-constraints.md:上传上限 50MB → **硬 5 MiB**(常量、无配置项)
This commit is contained in:
AskaEth committed 2026-10-04 14:13:27 +08:00
1 parent 5fb0cf5e19
commit 20d452a51a
4 files changed
+198 -20

No files matched your search

+9 -4
View File
@@ -42,8 +42,13 @@ const maxSessionTitleRunes = 80
// handlePatchSession 更新持久化会话的显示元数据 —— 其标题
// (A4)与置顶标志(D5)。二者都是元数据:与 DELETE 不同,它们
// 允许作用于固定的主会话。两个字段都不携带的请求返回 400,
// 空/仅空白或过长的标题亦然,未知会话 id(照常 404/500)。
// 允许作用于固定的主会话。未知会话 id 照常 404/500。
//
// 两种 400 刻意用不同文案区分「缺字段」与「字段在、值非法」:
// title 与 pinned 都没给 → 「至少需要 title 或 pinned」;
// 给了 title 但 trim 后为空白 → 「title 不得为空白」;过长 → 明确的上限提示。
// title 的空串/仅空白**没有**清空语义(不回落自动命名),
// 因而客户端不提供「恢复自动命名」入口;要换名只能给非空白标题。
//
// 携带 personaId 键的请求体对任何取值都返回 400,且该检查
// 在会话被查找之前执行:会话角色在创建时固定(design/22
@@ -74,14 +79,14 @@ func (s *Server) handlePatchSession(w http.ResponseWriter, r *http.Request) {
return
}
if req.Title == nil && req.Pinned == nil {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "title or pinned is required"))
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "at least one of title or pinned is required"))
return
}
resp := map[string]any{"id": id}
if req.Title != nil {
title := strings.TrimSpace(*req.Title)
if title == "" {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "title is required"))
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "title must not be blank"))
return
}
if utf8.RuneCountInString(title) > maxSessionTitleRunes {