feat(history): 消息搜索/编辑/删除 + 会话置顶(D2/D3/D5)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- history/message_ops.go(新): Search(会话内, q/after互斥, LIKE参数化+%/_转义+大小写不敏感, 严格限定会话doc) / UpdateMessage(返回更新后Message) / DeleteMessage / messageByID / escapeLike
- history/session_pin.go(新): SetPinned; 迁移0009 chat_sessions加pinned INTEGER DEFAULT 0; store.go SessionMeta.Pinned + createSessionsTable加列
- session_preview.go: 查询加s.pinned + ORDER BY pinned DESC,(type=main) DESC,created_at ASC
- gateway history_write.go(新,保持history.go≤500): handlePatchSession(pinned+title) / handlePatchMessage / handleDeleteMessage; server.go加PATCH/DELETE /api/v1/messages/{id} + /sessions/{id}置顶
- history.go: handleListMessages读q分流Search; 契约PATCH title/置顶
- D2会话内搜索(不做跨会话,隔离会话不外泄红线); D3不做重发端点(客户端编辑后再发); D5用pinned bool
- 测试(message_ops/history_messages/migrate_pinned), build/vet/test绿+3平台交叉编译; Docs/api-client v0.30
- 未做: 编辑/删除WS实时广播(message_edited/deleted), 下批
This commit is contained in:
AskaEth committed 2026-09-08 23:59:13 +08:00
1 parent c7b2cd8ae0
commit 844b2a74c1
15 files changed
+1091 -73

No files matched your search

+29 -1
View File
@@ -62,6 +62,10 @@ var (
// ErrImmutableMain is returned when a caller tries to delete the fixed main session.
ErrImmutableMain = errors.New("history: the main session cannot be deleted")
// ErrMessageNotFound is returned by UpdateMessage/DeleteMessage when no
// chat_messages row carries the requested id.
ErrMessageNotFound = errors.New("history: message not found")
)
// Message is one persisted chat message. CreatedAt is a Unix millisecond timestamp and
@@ -109,6 +113,9 @@ type SessionMeta struct {
Type string // "main" | "sub"
Title string
CreatedAt int64
// Pinned floats the session to the top of ListSessions. It is display/order
// metadata only; identity and permissions never derive from it.
Pinned bool
// LastMessage is a display-only preview of the session's newest message, filled
// by ListSessions in the same query. It is nil for a session with no messages.
// A client uses it to render the conversation row without a per-session request;
@@ -161,13 +168,33 @@ type HistoryStore interface {
// DeleteSession removes a sub session and all its messages. The main session is
// immutable and returns ErrImmutableMain.
DeleteSession(ctx context.Context, sessionID string) error
// Search returns up to limit messages for sessionID whose content contains query
// (case-insensitive ASCII LIKE, with %/_/\ escaped), newest first. before is the
// same millisecond cursor as List; limit is clamped to [1, maxPageLimit]. An empty
// query returns an empty slice (the caller falls back to List for plain paging).
// Search is always scoped to one session, so an isolated group/stranger session can
// never surface in another session's results.
Search(ctx context.Context, sessionID, query string, before int64, limit int) ([]Message, error)
// UpdateMessage replaces the content of the message with id and returns the
// updated message (like AppendFull, so the caller can broadcast it without a
// second read). Only content changes; role/timestamps/platform stay as stored.
// An unknown id returns ErrMessageNotFound.
UpdateMessage(ctx context.Context, id, content string) (Message, error)
// DeleteMessage removes a single message. An unknown id returns
// ErrMessageNotFound.
DeleteMessage(ctx context.Context, id string) error
// RenameSession sets a session's display title. Both the main session and sub
// sessions may be renamed (a title is display-only metadata); an unknown id
// returns ErrSessionNotFound.
RenameSession(ctx context.Context, sessionID, title string) error
// SetPinned pins or unpins a session. Pinning is display/order metadata only;
// both the main session and sub sessions may be pinned. An unknown id returns
// ErrSessionNotFound.
SetPinned(ctx context.Context, sessionID string, pinned bool) error
// NewSubSession creates a new sub session and returns its id (MainSessionID:uuid).
NewSubSession(ctx context.Context, ownerID string) (string, error)
// ListSessions returns the main session first then every sub session, oldest first.
// ListSessions returns pinned sessions first, then the main session, then every
// sub session oldest first.
ListSessions(ctx context.Context) ([]SessionMeta, error)
}
@@ -551,6 +578,7 @@ CREATE TABLE IF NOT EXISTS chat_sessions (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
pinned INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
)`