feat(gateway+web): D 消息编辑/删除 WS 实时广播 — message_edited/message_deleted
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 后端: ws_hub.go broadcastMessageEdited(完整DTO,thinking剥离)/broadcastMessageDeleted(全广播+sessionId,前端过滤); history_write.go PATCH/DELETE成功后触发(404/400/501不广播); history/message_ops.go GetMessage(导出壳,取删除前sessionId)
- 前端: useAppStore.updateMessageById/removeMessageById + messageEventApplies(同会话过滤); ws.ts 消费两事件(缺content丢弃); dispatchFrame导出(可测)
- 真机e2e: PATCH/DELETE → WS收到 message_edited/message_deleted, 二次删404无帧
- Docs/api-client v0.34 §3.8; 测试 history_write_broadcast_test + messageSync.test(14 checks)
- 遗留: 前端编辑/删除UI入口+api客户端函数未接(lib/api 无 PATCH/DELETE messages); ws_hub.go 589行>500(既有)
This commit is contained in:
AskaEth committed 2026-09-30 11:58:49 +08:00
1 parent 4ea68d17f3
commit 2720d77331
8 files changed
+485 -15

No files matched your search

+13 -4
View File
@@ -9,10 +9,11 @@ import (
)
// This file owns the single-message operations behind the Android collaboration batch
// ③a (D2 search, D3 edit/delete). Both are strictly session-scoped or id-scoped: a
// message is only ever reached through the session it belongs to (Search) or its own
// opaque id (UpdateMessage/DeleteMessage), so an isolated group/stranger session can
// never surface in another session's results (project red line).
// ③a (D2 search, D3 edit/delete) plus the id-scoped read the D delete broadcast needs.
// All are strictly session-scoped or id-scoped: a message is only ever reached through
// the session it belongs to (Search) or its own opaque id (GetMessage/UpdateMessage/
// DeleteMessage), so an isolated group/stranger session can never surface in another
// session's results (project red line).
// Search returns up to limit messages for sessionID whose content contains query,
// newest first. The match is a case-insensitive ASCII LIKE (SQLite's default for
@@ -91,6 +92,14 @@ func (s *Store) DeleteMessage(ctx context.Context, id string) error {
return nil
}
// GetMessage reads the message with id, returning ErrMessageNotFound when no such
// row exists. It is the id-scoped read the transport uses before removing a
// message: DeleteMessage leaves nothing to ask afterwards, yet the broadcast that
// tells other clients to drop the row is session-scoped.
func (s *Store) GetMessage(ctx context.Context, id string) (Message, error) {
return s.messageByID(ctx, id)
}
// messageByID reads one message by id, returning ErrMessageNotFound when the row is
// absent.
func (s *Store) messageByID(ctx context.Context, id string) (Message, error) {