docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
This commit is contained in:
AskaEth committed 2026-10-03 22:28:25 +08:00
1 parent 91e8d7a029
commit 074ffc4ebf
73 files changed
+2529 -2529

No files matched your search

+57 -57
View File
@@ -12,47 +12,47 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
)
// This file owns the session/message write endpoints added for the Android
// collaboration batch ③a: PATCH /sessions/{id} (A4 rename + D5 pin), PATCH
// /messages/{id} (D3 edit) and DELETE /messages/{id} (D3 delete). All are owner/admin
// history writes; the reads and DTOs live in history.go. The D3 edit/delete paths also
// broadcast their outcome over the WS hub (message_edited / message_deleted, see
// ws_hub.go) so the account's other browsers stay in sync without a manual re-read.
// 本文件拥有为 Android 协作批次 ③a 新增的会话/消息写端点:
// PATCH /sessions/{id}(A4 改名 + D5 置顶)、PATCH
// /messages/{id}(D3 编辑)与 DELETE /messages/{id}(D3 删除)。它们都是 owner/admin
// 历史写;读与 DTO 位于 history.go。D3 的编辑/删除路径还会
// 经 WS hub 广播其结果(message_edited / message_deleted,见
// ws_hub.go),使该账号的其他浏览器无需手动重读即保持同步。
// patchSessionRequest is the PATCH /api/v1/sessions/{id} body. Both fields are
// pointers so an absent field is distinguishable from an explicit zero value:
// title renames the session (A4), pinned sets/unets the pin (D5), and at least one
// must be present.
// patchSessionRequest 是 PATCH /api/v1/sessions/{id} 的请求体。两个字段都是
// 指针,因此字段缺失与显式零值可区分:title 重命名该
// 会话(A4),pinned 设置/取消置顶(D5),且至少一个
// 必须出现。
//
// personaId is deliberately NOT a settable field: a session's role is fixed when the
// session is created (design/22 §8-1), so the key is rejected outright instead of
// being ignored. It stays in the struct as the raw JSON so the handler can tell
// "the key is absent" from "the key is present with any value" — `"yuki"`, `""` and
// `null` are all refused, and only a body without the key is accepted.
// personaId 刻意不是可设置字段:会话角色在会话创建时
// 固定(design/22 §8-1),因此该键会被直接拒绝,而不是
// 被忽略。它以原始 JSON 留在结构体中,使 handler 能区分
// 「键缺失」与「键存在且为任意值」—— `"yuki"`、`""` 与
// `null` 一律被拒,只有不含该键的请求体才被接受。
type patchSessionRequest struct {
Title *string `json:"title"`
Pinned *bool `json:"pinned"`
PersonaID json.RawMessage `json:"personaId"`
}
// maxSessionTitleRunes bounds a client-supplied session title so a display row never
// overflows. It is a display-only bound: identity and permissions never derive from
// the title (project red line).
// maxSessionTitleRunes 限定客户端提供的会话标题,使显示行绝不
// 溢出。它只是显示边界:身份与权限绝不派生自
// 标题(项目红线)。
const maxSessionTitleRunes = 80
// handlePatchSession updates a persistent session's display metadata — its title
// (A4) and its pinned flag (D5). Both are metadata: unlike DELETE they are allowed
// on the fixed main session. A request carrying neither field answers 400, as do an
// empty/whitespace or over-long title and an unknown session id (404/500 as usual).
// handlePatchSession 更新持久化会话的显示元数据 —— 其标题
// (A4)与置顶标志(D5)。二者都是元数据:与 DELETE 不同,它们
// 允许作用于固定的主会话。两个字段都不携带的请求返回 400,
// 空/仅空白或过长的标题亦然,未知会话 id(照常 404/500)。
//
// A body carrying the personaId key answers 400 for EVERY value, and that check runs
// before the session is looked up: a session's role is fixed at creation (design/22
// §8-1), so switching the role of an existing conversation — including clearing it —
// is not a supported operation; create a new session instead. Refusing explicitly
// (rather than ignoring the key) is what keeps a client from believing a rebind
// landed. It is owner/admin-gated like the other history writes. The response echoes
// only the fields that were changed, so a title-only PATCH keeps its pre-D5
// {id,title} shape.
// 携带 personaId 键的请求体对任何取值都返回 400,且该检查
// 在会话被查找之前执行:会话角色在创建时固定(design/22
// §8-1),因此切换既有对话的角色 —— 包括清除它 ——
// 不是受支持的操作;请改为新建会话。显式拒绝
// (而不是忽略该键)正是让客户端不会误以为重绑
// 生效的原因。它与其他历史写一样受 owner/admin 门控。响应只
// 回显被改动的字段,因此纯标题 PATCH 保持其 D5 之前的
// {id,title} 形状。
func (s *Server) handlePatchSession(w http.ResponseWriter, r *http.Request) {
if s.history == nil {
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "会话历史服务未配置(未注入 HistoryStore)", HTTPCode: http.StatusNotImplemented})
@@ -104,8 +104,8 @@ func (s *Server) handlePatchSession(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, resp)
}
// writeSessionPatchError maps a RenameSession/SetPinned failure to its response:
// ErrSessionNotFound → 404, anything else → 500 (logged under op).
// writeSessionPatchError 把 RenameSession/SetPinned 的失败映射为其响应:
// ErrSessionNotFound → 404,其余 → 500(在 op 下记日志)。
func (s *Server) writeSessionPatchError(w http.ResponseWriter, err error, id, op string) {
if errors.Is(err, history.ErrSessionNotFound) {
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "session %q not found", id))
@@ -115,21 +115,21 @@ func (s *Server) writeSessionPatchError(w http.ResponseWriter, err error, id, op
writeError(w, apperr.Wrap(apperr.ErrInternal, "session_failed", "failed to update session"))
}
// patchMessageRequest is the PATCH /api/v1/messages/{id} body. Content is a pointer so
// an absent field is rejected rather than treated as an empty edit.
// patchMessageRequest 是 PATCH /api/v1/messages/{id} 的请求体。Content 是指针,
// 因此字段缺失会被拒绝,而不是被当作空编辑。
type patchMessageRequest struct {
Content *string `json:"content"`
}
// maxMessageContentRunes bounds an edited message body. It is a storage/display bound
// on the edit path only; inbound chat messages are not subject to it.
// maxMessageContentRunes 限定被编辑的消息体。它只是编辑路径上的
// 存储/显示边界;入站聊天消息不受其约束。
const maxMessageContentRunes = 4000
// handlePatchMessage edits a single message's content (Android collaboration batch ③a,
// D3). Only content changes — role/timestamp/platform stay as stored — and the response
// returns the updated message so the client can upsert it by id without a re-read. An
// empty/whitespace or over-long content answers 400, an unknown id answers 404, and a
// missing HistoryStore answers 501. It is owner/admin-gated.
// handlePatchMessage 编辑单条消息的内容(Android 协作批次 ③a,
// D3)。只有 content 会变 —— role/时间戳/platform 保持已存值 —— 响应
// 返回更新后的消息,使客户端能按 id upsert 而无需重读。
// 空/仅空白或过长的 content 返回 400,未知 id 返回 404,
// HistoryStore 缺失返回 501。它受 owner/admin 门控。
func (s *Server) handlePatchMessage(w http.ResponseWriter, r *http.Request) {
if s.history == nil {
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "会话历史服务未配置(未注入 HistoryStore)", HTTPCode: http.StatusNotImplemented})
@@ -168,20 +168,20 @@ func (s *Server) handlePatchMessage(w http.ResponseWriter, r *http.Request) {
writeError(w, apperr.Wrap(apperr.ErrInternal, "message_failed", "failed to update message"))
return
}
// Broadcast the edit so every other browser of the account re-renders the same
// bubble without a refresh (the message_persisted realtime-sync contract, D3 edit
// path). Thinking is stripped from the frame to match the persisted/replay
// contract; the REST reply keeps it because the messages routes are owner/admin-only.
// 广播该编辑,使该账号的其他每个浏览器无需刷新就重渲染同一个
// 气泡(message_persisted 实时同步契约,D3 编辑
// 路径)。thinking 会从帧中剥掉,以匹配持久化/重放
// 契约;REST 回复保留它,因为消息路由仅 owner/admin。
edited := toHistoryMessage(msg)
edited.Thinking = ""
s.hub.broadcastMessageEdited(edited)
writeJSON(w, http.StatusOK, toHistoryMessage(msg))
}
// handleDeleteMessage removes a single message (D3). An unknown id answers 404, a
// missing HistoryStore 501. It is owner/admin-gated. A successful delete is
// broadcast as a `message_deleted` frame so every browser of the account drops the
// row without a refresh; the owning session is read before the row disappears.
// handleDeleteMessage 移除单条消息(D3)。未知 id 返回 404,
// HistoryStore 缺失返回 501。它受 owner/admin 门控。成功的删除会
// 以 `message_deleted` 帧广播,使该账号的每个浏览器无需刷新就丢掉
// 该行;所属会话在该行消失前被读取。
func (s *Server) handleDeleteMessage(w http.ResponseWriter, r *http.Request) {
if s.history == nil {
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "会话历史服务未配置(未注入 HistoryStore)", HTTPCode: http.StatusNotImplemented})
@@ -206,18 +206,18 @@ func (s *Server) handleDeleteMessage(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"deleted": id})
}
// messageLookup is the optional narrower view of HistoryStore used by DELETE to learn
// a message's owning session before it is removed (afterwards there is nothing left to
// ask). *history.Store implements it; a store that does not simply yields an unscoped
// frame, which the client resolves by the globally unique message id.
// messageLookup 是 HistoryStore 可选的更窄视图,DELETE 用它
// 在移除前得知消息所属的会话(之后就没有可问的了)。
// *history.Store 实现了它;未实现的存储只是产出一个无作用域
// 的帧,客户端凭全局唯一的消息 id 解析它。
type messageLookup interface {
GetMessage(ctx context.Context, id string) (history.Message, error)
}
// messageSession returns the session id owning message id, or "" when the store cannot
// answer (no lookup surface, unknown id, or a read error). A blank session is safe for
// the broadcast: the message id is globally unique, so a client that falls back to an
// id-only filter can only ever touch that message's own conversation.
// messageSession 返回拥有 message id 的会话 id,存储无法作答时
// 返回 ""(无查找表面、未知 id 或读错误)。空白会话
// 对广播是安全的:消息 id 全局唯一,因此回退到
// 纯 id 过滤的客户端只可能触及该消息自己的对话。
func (s *Server) messageSession(ctx context.Context, id string) string {
lookup, ok := s.history.(messageLookup)
if !ok {