gitdataai/src/lib/ws-protocol.ts
ZhenYi 047782e585 fix(room): handle reaction updates in onRoomMessage WS handler
Root cause: publish_reaction_event sends a RoomMessageEvent (not
reaction_added) with reactions field set. The onRoomMessage handler
previously returned prev immediately when a duplicate ID was found,
skipping the reaction update entirely.

Fix:
- Add reactions field to RoomMessagePayload so TypeScript knows it's there
- When a duplicate message ID is found AND payload carries reactions,
  update the existing message's reactions field instead of ignoring
- ReactionItem and ReactionGroup have identical shapes, so assignment works
2026-04-17 22:37:20 +08:00

401 lines
8.1 KiB
TypeScript

export interface WsRequest {
type: 'request';
request_id: string;
action: WsAction;
params?: WsRequestParams;
}
export type WsAction =
| 'room.list'
| 'room.get'
| 'room.create'
| 'room.update'
| 'room.delete'
| 'category.list'
| 'category.create'
| 'category.update'
| 'category.delete'
| 'message.list'
| 'message.create'
| 'message.update'
| 'message.revoke'
| 'message.get'
| 'member.list'
| 'member.add'
| 'member.remove'
| 'member.leave'
| 'member.set_read_seq'
| 'member.update_role'
| 'pin.list'
| 'pin.add'
| 'pin.remove'
| 'thread.list'
| 'thread.create'
| 'thread.messages'
| 'reaction.add'
| 'reaction.remove'
| 'reaction.get'
| 'message.search'
| 'message.edit_history'
| 'ai.list'
| 'ai.upsert'
| 'ai.delete'
| 'notification.list'
| 'notification.mark_read'
| 'notification.mark_all_read'
| 'notification.archive'
| 'mention.list'
| 'mention.read_all'
| 'room.subscribe'
| 'room.unsubscribe'
| 'project.subscribe'
| 'project.unsubscribe'
| 'reaction.list_batch';
export interface WsRequestParams {
project_name?: string;
room_id?: string;
message_id?: string;
user_id?: string;
category_id?: string;
thread_id?: string;
model_id?: string;
notification_id?: string;
emoji?: string;
only_public?: boolean;
only_unread?: boolean;
limit?: number;
offset?: number;
before_seq?: number;
after_seq?: number;
room_name?: string;
room_public?: boolean;
room_category?: string;
content?: string;
content_type?: string;
in_reply_to?: string;
parent_seq?: number;
role?: string;
last_read_seq?: number;
name?: string;
position?: number;
model?: string;
model_version?: string;
history_limit?: number;
system_prompt?: string;
temperature?: number;
max_tokens?: number;
use_exact?: boolean;
think?: boolean;
stream?: boolean;
min_score?: number;
query?: string;
message_ids?: string[];
}
export interface WsResponse {
type: 'response';
request_id: string;
action: string;
data?: WsResponseData;
error?: WsErrorInfo | null;
}
export interface WsErrorInfo {
code: number;
error: string;
message: string;
}
/** 响应数据类型 */
export type WsResponseData =
| boolean
| number
| RoomResponse
| RoomResponse[]
| RoomCategoryResponse
| RoomCategoryResponse[]
| RoomMessageResponse
| RoomMessageListResponse
| RoomMemberResponse
| RoomMemberResponse[]
| PinResponseData
| PinResponseData[]
| RoomThreadResponse
| RoomThreadResponse[]
| ReactionListData
| ReactionListData[]
| SearchResultData
| MessageEditHistoryResponse
| AiConfigData
| AiConfigData[]
| NotificationListData
| MentionListData
| SubscribeData
| UserInfo[];
export interface WsEvent {
type: 'event';
event: string;
room_id?: string | null;
project_id?: string | null;
data?: unknown;
error?: string | null;
}
/** Maps backend event string to typed event. Only room-relevant events are typed. */
export type WsEventPayload =
| { type: 'room_message'; data: RoomMessagePayload }
| { type: 'project_event'; data: ProjectEventPayload }
| { type: 'ai_stream_chunk'; data: AiStreamChunkPayload }
| { type: 'room_reaction_updated'; data: RoomReactionUpdatedPayload }
| { type: 'message_edited'; data: MessageEditedPayload }
| { type: 'message_revoked'; data: MessageRevokedPayload }
| { type: 'message_pinned'; data: MessagePinnedPayload }
| { type: 'message_unpinned'; data: MessageUnpinnedPayload }
| { type: string; data: unknown }; // catch-all for unknown events
export interface RoomMessagePayload {
id: string;
room_id: string;
sender_type: string;
sender_id?: string;
thread_id?: string;
content: string;
content_type: string;
send_at: string;
seq: number;
display_name?: string;
/** Present when this event carries reaction updates for the message */
reactions?: ReactionItem[];
}
export interface ProjectEventPayload {
event_type: string;
project_id: string;
room_id?: string;
category_id?: string;
message_id?: string;
seq?: number;
timestamp: string;
}
export interface AiStreamChunkPayload {
message_id: string;
room_id: string;
content: string;
done: boolean;
error?: string;
}
export interface RoomResponse {
id: string;
project: string;
room_name: string;
public: boolean;
category?: string;
created_by: string;
created_at: string;
last_msg_at: string;
unread_count: number;
}
export interface RoomCategoryResponse {
id: string;
project: string;
name: string;
position: number;
created_by: string;
created_at: string;
}
export interface RoomMessageResponse {
id: string;
seq: number;
room: string;
sender_type: string;
sender_id?: string;
display_name?: string;
thread?: string;
in_reply_to?: string;
content: string;
content_type: string;
edited_at?: string;
send_at: string;
revoked?: string;
revoked_by?: string;
}
export interface RoomMessageListResponse {
messages: RoomMessageResponse[];
total: number;
}
export interface RoomMemberResponse {
room: string;
user: string;
user_info?: UserInfo;
role: string;
first_msg_in?: string;
joined_at?: string;
last_read_seq?: number;
do_not_disturb: boolean;
dnd_start_hour?: number;
dnd_end_hour?: number;
}
export interface UserInfo {
uid: string;
username: string;
avatar_url?: string;
}
export interface PinResponseData {
room: string;
message: string;
pinned_by: string;
pinned_at: string;
message_data?: RoomMessageResponse;
}
export interface RoomThreadResponse {
id: string;
room: string;
parent: number;
created_by: string;
participants: unknown;
last_message_at: string;
last_message_preview?: string;
created_at: string;
updated_at: string;
}
export interface ReactionItem {
emoji: string;
count: number;
reacted_by_me: boolean;
users: string[];
}
export interface ReactionListData {
message_id: string;
reactions: ReactionItem[];
}
export interface RoomReactionUpdatedPayload {
message_id: string;
reactions: ReactionItem[];
}
export interface MessageEditedPayload {
message_id: string;
room_id: string;
edited_at: string;
}
export interface MessageRevokedPayload {
message_id: string;
room_id: string;
revoked_at: string;
revoked_by: string;
}
export interface MessagePinnedPayload {
message_id: string;
room_id: string;
pinned_by: string;
pinned_at: string;
}
export interface MessageUnpinnedPayload {
message_id: string;
room_id: string;
}
export interface SearchResultData {
messages: RoomMessageResponse[];
total: number;
}
export interface EditHistoryEntry {
old_content: string;
new_content: string;
edited_at: string;
}
export interface MessageEditHistoryResponse {
message_id: string;
history: EditHistoryEntry[];
total_edits: number;
}
export interface AiConfigData {
room: string;
model: string;
version?: string;
call_count: number;
last_call_at?: string;
history_limit?: number;
system_prompt?: string;
temperature?: number;
max_tokens?: number;
use_exact: boolean;
think: boolean;
stream: boolean;
min_score?: number;
created_at: string;
updated_at: string;
}
export interface NotificationData {
id: string;
room?: string;
project?: string;
user_id?: string;
user_info?: UserInfo;
notification_type: string;
title: string;
content?: string;
related_message_id?: string;
related_user_id?: string;
related_room_id?: string;
is_read: boolean;
is_archived: boolean;
created_at: string;
read_at?: string;
expires_at?: string;
}
export interface NotificationListData {
notifications: NotificationData[];
total: number;
unread_count: number;
}
export interface MentionListData {
mentions: MentionData[];
}
export interface MentionData {
message_id: string;
mentioned_by: string;
mentioned_by_name: string;
content_preview: string;
room_id: string;
room_name: string;
created_at: string;
}
export interface SubscribeData {
room_id?: string;
project_id?: string;
}
export type WsInMessage = WsResponse | WsEvent | WsErrorMessage;
export interface WsErrorMessage {
type: 'error';
error: string;
message?: string;
}