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
401 lines
8.1 KiB
TypeScript
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;
|
|
}
|