refactor: move websocket and message queue to core/net

Reorganize project structure by relocating chat-websocket and message-queue modules from `src/integrations/` to `src/core/net/` to better reflect their networking purpose and improve code organization.
This commit is contained in:
2026-06-10 17:44:35 +08:00
parent a511beaa34
commit aa1688140a
3 changed files with 2 additions and 2 deletions
-152
View File
@@ -1,152 +0,0 @@
/**
* 聊天 WebSocket 处理器
*
* 原始 Dart: lib/core/net/websocket_handler.dart
*
* 浏览器方案:使用原生 `WebSocket` API,封装连接管理 + 重连 + 事件回调。
*
* 事件:
* - onConnected(userId)
* - onTyping(isTyping)
* - onSentence(index, text, total, done) ← AI 逐句推送
* - onVoiceReady(index, audioUrl)
* - onIntimacyUpdate(intimacyChange, newIntimacy, relationshipStage)
* - onMoodUpdate(mood)
* - onError(errorMessage)
*/
import { getApiConfig } from "@/core/net/config/api_config";
export interface SentencePayload {
index: number;
text: string;
total: number;
done: boolean;
}
export interface IntimacyPayload {
intimacyChange?: number;
newIntimacy?: number;
relationshipStage?: string;
}
export class ChatWebSocket {
private ws: WebSocket | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private disposed = false;
onConnected: ((userId: string) => void) | null = null;
onTyping: ((isTyping: boolean) => void) | null = null;
onSentence: ((payload: SentencePayload) => void) | null = null;
onVoiceReady: ((index: number, audioUrl: string) => void) | null = null;
onIntimacyUpdate: ((payload: IntimacyPayload) => void) | null = null;
onMoodUpdate: ((mood: string) => void) | null = null;
onError: ((errorMessage: string) => void) | null = null;
constructor(
private readonly serverUrl: string,
private readonly token: string,
) {}
get isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
connect(): void {
if (this.disposed) return;
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
try {
const url = `${this.serverUrl}?token=${encodeURIComponent(this.token)}`;
this.ws = new WebSocket(url);
this.ws.onopen = () => {
// 连接成功;userId 由后端在第一条消息中下发
};
this.ws.onmessage = (e) => this.handleMessage(e.data);
this.ws.onerror = () => this.onError?.("WebSocket error");
this.ws.onclose = () => {
if (!this.disposed) {
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
}
};
} catch (e) {
this.onError?.(e instanceof Error ? e.message : String(e));
}
}
sendMessage(content: string): boolean {
if (!this.isConnected) return false;
this.ws?.send(JSON.stringify({ type: "message", content }));
return true;
}
disconnect(): void {
this.disposed = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.ws?.close();
this.ws = null;
}
private handleMessage(data: unknown): void {
if (typeof data !== "string") return;
let payload: {
type?: string;
userId?: string;
isTyping?: boolean;
index?: number;
text?: string;
total?: number;
done?: boolean;
audioUrl?: string;
intimacyChange?: number;
newIntimacy?: number;
relationshipStage?: string;
mood?: string;
error?: string;
};
try {
payload = JSON.parse(data);
} catch {
return;
}
switch (payload.type) {
case "connected":
if (payload.userId) this.onConnected?.(payload.userId);
break;
case "typing":
this.onTyping?.(payload.isTyping ?? false);
break;
case "sentence":
this.onSentence?.({
index: payload.index ?? 0,
text: payload.text ?? "",
total: payload.total ?? 0,
done: payload.done ?? false,
});
break;
case "voice_ready":
this.onVoiceReady?.(payload.index ?? 0, payload.audioUrl ?? "");
break;
case "intimacy_update":
this.onIntimacyUpdate?.({
intimacyChange: payload.intimacyChange,
newIntimacy: payload.newIntimacy,
relationshipStage: payload.relationshipStage,
});
break;
case "mood_update":
if (payload.mood) this.onMoodUpdate?.(payload.mood);
break;
case "error":
this.onError?.(payload.error ?? "Unknown error");
break;
}
}
}
/** 默认创建实例:从 env 读取 WS URL。 */
export function createChatWebSocket(token: string): ChatWebSocket {
const base = getApiConfig().wsUrl;
return new ChatWebSocket(base, token);
}
+2 -2
View File
@@ -3,6 +3,6 @@
*/
export * from "./browser-detect";
export * from "./chat-websocket";
export * from "../core/net/chat-websocket";
export * from "./media-recorder";
export * from "./message-queue";
export * from "../core/net/message-queue";
-74
View File
@@ -1,74 +0,0 @@
/**
* 消息发送队列(限流)
*
* 原始 Dart: lib/core/net/message_queue.dart
*
* 业务行为:
* - 入队消息按 `intervalMs` 间隔逐条发送(默认 1000ms)
* - 消费者可声明每条消息是否成功消费;不成功则重新入队
* - 队列可在任意时刻清空(dispose)
*/
export type MessageConsumer = (content: string) => boolean | Promise<boolean>;
export class MessageQueue {
private queue: string[] = [];
private timer: ReturnType<typeof setTimeout> | null = null;
private disposed = false;
private consumer: MessageConsumer | null = null;
constructor(private readonly intervalMs = 1000) {}
/** 注册消费者(消费者必须能处理失败/重试)。 */
setConsumer(consumer: MessageConsumer): void {
this.consumer = consumer;
}
enqueue(content: string): void {
if (this.disposed) return;
this.queue.push(content);
if (!this.timer) this.scheduleNext();
}
private async attemptSend(next: string): Promise<void> {
if (!this.consumer) {
this.queue.unshift(next);
return;
}
let ok = false;
try {
ok = await this.consumer(next);
} catch (e) {
console.error("[MessageQueue] consumer error", e);
ok = false;
}
if (!ok) {
// 消费失败,放回队首等待下次
this.queue.unshift(next);
}
}
private scheduleNext(): void {
if (this.disposed) return;
const next = this.queue.shift();
if (next === undefined) {
this.timer = null;
return;
}
this.timer = setTimeout(() => {
void this.attemptSend(next).finally(() => this.scheduleNext());
}, this.intervalMs);
}
clear(): void {
this.queue = [];
}
dispose(): void {
this.disposed = true;
this.clear();
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}