@zhin.js/client 1.0.17 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/app.ts +228 -0
- package/client/bootstrap/loadConsoleEntries.test.ts +32 -0
- package/client/bootstrap/loadConsoleEntries.ts +149 -0
- package/client/console-utils/cn.ts +6 -0
- package/client/console-utils/remoteApi.ts +48 -0
- package/client/index.ts +61 -14
- package/client/persistence/idb-store.ts +76 -0
- package/client/runtime/index.ts +12 -0
- package/client/store/createRegistryStore.ts +46 -0
- package/client/websocket/hooks.ts +218 -510
- package/client/websocket/index.ts +6 -43
- package/client/websocket/instance.ts +10 -32
- package/client/websocket/manager.ts +301 -385
- package/client/websocket/remote-settings.ts +25 -0
- package/client/websocket/types.ts +58 -222
- package/dist/app.d.ts +95 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +147 -0
- package/dist/app.js.map +1 -0
- package/dist/bootstrap/loadConsoleEntries.d.ts +26 -0
- package/dist/bootstrap/loadConsoleEntries.d.ts.map +1 -0
- package/dist/bootstrap/loadConsoleEntries.js +111 -0
- package/dist/bootstrap/loadConsoleEntries.js.map +1 -0
- package/dist/bootstrap/loadConsoleEntries.test.d.ts +2 -0
- package/dist/bootstrap/loadConsoleEntries.test.d.ts.map +1 -0
- package/dist/bootstrap/loadConsoleEntries.test.js +27 -0
- package/dist/bootstrap/loadConsoleEntries.test.js.map +1 -0
- package/dist/console-utils/cn.d.ts +3 -0
- package/dist/console-utils/cn.d.ts.map +1 -0
- package/dist/console-utils/cn.js +6 -0
- package/dist/console-utils/cn.js.map +1 -0
- package/dist/console-utils/remoteApi.d.ts +7 -0
- package/dist/console-utils/remoteApi.d.ts.map +1 -0
- package/dist/console-utils/remoteApi.js +41 -0
- package/dist/console-utils/remoteApi.js.map +1 -0
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -14
- package/dist/index.js.map +1 -1
- package/dist/persistence/idb-store.d.ts +15 -0
- package/dist/persistence/idb-store.d.ts.map +1 -0
- package/dist/persistence/idb-store.js +61 -0
- package/dist/persistence/idb-store.js.map +1 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/store/createRegistryStore.d.ts +9 -0
- package/dist/store/createRegistryStore.d.ts.map +1 -0
- package/dist/store/createRegistryStore.js +38 -0
- package/dist/store/createRegistryStore.js.map +1 -0
- package/dist/websocket/hooks.d.ts +35 -58
- package/dist/websocket/hooks.d.ts.map +1 -1
- package/dist/websocket/hooks.js +134 -410
- package/dist/websocket/hooks.js.map +1 -1
- package/dist/websocket/index.d.ts +5 -10
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +5 -25
- package/dist/websocket/index.js.map +1 -1
- package/dist/websocket/instance.d.ts +1 -14
- package/dist/websocket/instance.d.ts.map +1 -1
- package/dist/websocket/instance.js +2 -20
- package/dist/websocket/instance.js.map +1 -1
- package/dist/websocket/manager.d.ts +28 -93
- package/dist/websocket/manager.d.ts.map +1 -1
- package/dist/websocket/manager.js +213 -221
- package/dist/websocket/manager.js.map +1 -1
- package/dist/websocket/remote-settings.d.ts +5 -0
- package/dist/websocket/remote-settings.d.ts.map +1 -0
- package/dist/websocket/remote-settings.js +23 -0
- package/dist/websocket/remote-settings.js.map +1 -0
- package/dist/websocket/types.d.ts +35 -122
- package/dist/websocket/types.d.ts.map +1 -1
- package/dist/websocket/types.js +4 -13
- package/dist/websocket/types.js.map +1 -1
- package/package.json +13 -11
- package/client/router/index.tsx +0 -54
- package/client/store/index.ts +0 -132
- package/client/store/reducers/config.ts +0 -135
- package/client/store/reducers/index.ts +0 -19
- package/client/store/reducers/route.ts +0 -126
- package/client/store/reducers/script.ts +0 -109
- package/client/store/reducers/ui.ts +0 -31
- package/client/websocket/messageHandler.ts +0 -208
- package/dist/router/index.d.ts +0 -25
- package/dist/router/index.d.ts.map +0 -1
- package/dist/router/index.js +0 -48
- package/dist/router/index.js.map +0 -1
- package/dist/store/index.d.ts +0 -19
- package/dist/store/index.d.ts.map +0 -1
- package/dist/store/index.js +0 -67
- package/dist/store/index.js.map +0 -1
- package/dist/store/reducers/config.d.ts +0 -54
- package/dist/store/reducers/config.d.ts.map +0 -1
- package/dist/store/reducers/config.js +0 -78
- package/dist/store/reducers/config.js.map +0 -1
- package/dist/store/reducers/index.d.ts +0 -13
- package/dist/store/reducers/index.d.ts.map +0 -1
- package/dist/store/reducers/index.js +0 -11
- package/dist/store/reducers/index.js.map +0 -1
- package/dist/store/reducers/route.d.ts +0 -30
- package/dist/store/reducers/route.d.ts.map +0 -1
- package/dist/store/reducers/route.js +0 -85
- package/dist/store/reducers/route.js.map +0 -1
- package/dist/store/reducers/script.d.ts +0 -13
- package/dist/store/reducers/script.d.ts.map +0 -1
- package/dist/store/reducers/script.js +0 -78
- package/dist/store/reducers/script.js.map +0 -1
- package/dist/store/reducers/ui.d.ts +0 -8
- package/dist/store/reducers/ui.d.ts.map +0 -1
- package/dist/store/reducers/ui.js +0 -23
- package/dist/store/reducers/ui.js.map +0 -1
- package/dist/websocket/messageHandler.d.ts +0 -58
- package/dist/websocket/messageHandler.d.ts.map +0 -1
- package/dist/websocket/messageHandler.js +0 -174
- package/dist/websocket/messageHandler.js.map +0 -1
|
@@ -1,510 +1,426 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket 核心管理器
|
|
3
|
-
* 负责 WebSocket 连接管理、消息发送、重连逻辑等核心功能
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { store, setConnected, updateConfigs, updateSchemas } from '../store'
|
|
7
|
-
import { MessageHandler } from './messageHandler'
|
|
8
1
|
import type {
|
|
9
2
|
WebSocketMessage,
|
|
10
3
|
WebSocketConfig,
|
|
11
4
|
WebSocketCallbacks,
|
|
12
|
-
|
|
13
|
-
DatabaseInfo,
|
|
14
|
-
TableInfo,
|
|
15
|
-
SelectResult,
|
|
16
|
-
KvEntry
|
|
17
|
-
} from './types'
|
|
5
|
+
} from "./types";
|
|
18
6
|
import {
|
|
19
7
|
ConnectionState,
|
|
20
8
|
WebSocketError,
|
|
21
9
|
ConnectionError,
|
|
22
10
|
MessageError,
|
|
23
|
-
RequestTimeoutError
|
|
24
|
-
} from
|
|
11
|
+
RequestTimeoutError,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { getApiBase, getStoredToken, resolveApiUrl } from "./remote-settings.js";
|
|
14
|
+
import { applyConsoleEvent } from "../persistence/idb-store.js";
|
|
25
15
|
|
|
26
16
|
export class WebSocketManager {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
private requestId = 0
|
|
42
|
-
private pendingRequests = new Map<number, {
|
|
43
|
-
resolve: (value: any) => void
|
|
44
|
-
reject: (error: Error) => void
|
|
45
|
-
timer: NodeJS.Timeout
|
|
46
|
-
}>()
|
|
47
|
-
|
|
48
|
-
// ============================================================================
|
|
49
|
-
// 构造函数
|
|
50
|
-
// ============================================================================
|
|
17
|
+
private ws: WebSocket | null = null;
|
|
18
|
+
private config: Required<WebSocketConfig>;
|
|
19
|
+
private callbacks: WebSocketCallbacks;
|
|
20
|
+
private state: ConnectionState = ConnectionState.DISCONNECTED;
|
|
21
|
+
private reconnectAttempts = 0;
|
|
22
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
23
|
+
private requestId = 0;
|
|
24
|
+
private pendingRequests = new Map<
|
|
25
|
+
number,
|
|
26
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }
|
|
27
|
+
>();
|
|
28
|
+
private connectedListeners = new Set<(connected: boolean) => void>();
|
|
29
|
+
private sseAbort: AbortController | null = null;
|
|
30
|
+
private useRestTransport = true;
|
|
51
31
|
|
|
52
32
|
constructor(config: WebSocketConfig = {}, callbacks: WebSocketCallbacks = {}) {
|
|
53
33
|
this.config = {
|
|
54
34
|
url: this.buildWebSocketUrl(config.url),
|
|
55
35
|
reconnectInterval: config.reconnectInterval ?? 3000,
|
|
56
36
|
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
57
|
-
requestTimeout: config.requestTimeout ?? 10000
|
|
58
|
-
}
|
|
59
|
-
this.callbacks = callbacks
|
|
37
|
+
requestTimeout: config.requestTimeout ?? 10000,
|
|
38
|
+
};
|
|
39
|
+
this.callbacks = callbacks;
|
|
60
40
|
}
|
|
61
41
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
42
|
+
onConnectionChange(listener: (connected: boolean) => void): () => void {
|
|
43
|
+
this.connectedListeners.add(listener);
|
|
44
|
+
return () => this.connectedListeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private notifyConnection(connected: boolean) {
|
|
48
|
+
for (const l of this.connectedListeners) l(connected);
|
|
49
|
+
}
|
|
65
50
|
|
|
66
|
-
/**
|
|
67
|
-
* 建立 WebSocket 连接
|
|
68
|
-
*/
|
|
69
51
|
connect(): void {
|
|
70
|
-
if (this.
|
|
71
|
-
|
|
52
|
+
if (this.useRestTransport) {
|
|
53
|
+
void this.connectRestSse();
|
|
54
|
+
return;
|
|
72
55
|
}
|
|
56
|
+
if (this.state === ConnectionState.CONNECTED || this.state === ConnectionState.CONNECTING) return;
|
|
57
|
+
this.setState(ConnectionState.CONNECTING);
|
|
58
|
+
try {
|
|
59
|
+
this.ws = new WebSocket(this.config.url);
|
|
60
|
+
this.attachEventHandlers();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.handleConnectionError(new ConnectionError("Failed to create WebSocket", error as Error));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
private async connectRestSse(): Promise<void> {
|
|
67
|
+
const token = getStoredToken();
|
|
68
|
+
if (!token) {
|
|
69
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
70
|
+
this.notifyConnection(false);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.setState(ConnectionState.CONNECTING);
|
|
74
|
+
this.sseAbort?.abort();
|
|
75
|
+
this.sseAbort = new AbortController();
|
|
76
76
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const res = await fetch(resolveApiUrl("/api/events"), {
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
Accept: "text/event-stream",
|
|
81
|
+
},
|
|
82
|
+
signal: this.sseAbort.signal,
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok || !res.body) {
|
|
85
|
+
throw new ConnectionError(`SSE failed: HTTP ${res.status}`);
|
|
86
|
+
}
|
|
87
|
+
this.setState(ConnectionState.CONNECTED);
|
|
88
|
+
this.reconnectAttempts = 0;
|
|
89
|
+
this.notifyConnection(true);
|
|
90
|
+
this.callbacks.onConnect?.();
|
|
91
|
+
void this.pumpSse(res.body);
|
|
79
92
|
} catch (error) {
|
|
80
|
-
|
|
93
|
+
if ((error as Error).name === "AbortError") return;
|
|
94
|
+
this.handleConnectionError(
|
|
95
|
+
error instanceof ConnectionError ? error : new ConnectionError("SSE connect failed", error as Error),
|
|
96
|
+
);
|
|
97
|
+
this.scheduleReconnect();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async pumpSse(body: ReadableStream<Uint8Array>): Promise<void> {
|
|
102
|
+
const reader = body.getReader();
|
|
103
|
+
const decoder = new TextDecoder();
|
|
104
|
+
let buffer = "";
|
|
105
|
+
try {
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read();
|
|
108
|
+
if (done) break;
|
|
109
|
+
buffer += decoder.decode(value, { stream: true });
|
|
110
|
+
const parts = buffer.split("\n\n");
|
|
111
|
+
buffer = parts.pop() ?? "";
|
|
112
|
+
for (const part of parts) {
|
|
113
|
+
const line = part.split("\n").find((l) => l.startsWith("data: "));
|
|
114
|
+
if (!line) continue;
|
|
115
|
+
const json = line.slice(6);
|
|
116
|
+
try {
|
|
117
|
+
const message = JSON.parse(json) as WebSocketMessage;
|
|
118
|
+
void applyConsoleEvent(message);
|
|
119
|
+
this.handleBroadcast(message);
|
|
120
|
+
this.callbacks.onMessage?.(message);
|
|
121
|
+
} catch {
|
|
122
|
+
/* skip */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
/* closed */
|
|
128
|
+
} finally {
|
|
129
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
130
|
+
this.notifyConnection(false);
|
|
131
|
+
this.setState(ConnectionState.RECONNECTING);
|
|
132
|
+
this.scheduleReconnect();
|
|
133
|
+
}
|
|
81
134
|
}
|
|
82
135
|
}
|
|
83
136
|
|
|
84
|
-
/**
|
|
85
|
-
* 断开 WebSocket 连接
|
|
86
|
-
*/
|
|
87
137
|
disconnect(): void {
|
|
88
|
-
this.clearReconnectTimer()
|
|
89
|
-
this.clearPendingRequests()
|
|
90
|
-
|
|
138
|
+
this.clearReconnectTimer();
|
|
139
|
+
this.clearPendingRequests();
|
|
140
|
+
this.sseAbort?.abort();
|
|
141
|
+
this.sseAbort = null;
|
|
91
142
|
if (this.ws) {
|
|
92
|
-
this.ws.close()
|
|
93
|
-
this.ws = null
|
|
143
|
+
this.ws.close();
|
|
144
|
+
this.ws = null;
|
|
94
145
|
}
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
store.dispatch(setConnected(false))
|
|
146
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
147
|
+
this.notifyConnection(false);
|
|
98
148
|
}
|
|
99
149
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!this.isConnected()) {
|
|
105
|
-
throw new WebSocketError('WebSocket is not connected', 'NOT_CONNECTED')
|
|
150
|
+
send(message: unknown): void {
|
|
151
|
+
if (this.useRestTransport) {
|
|
152
|
+
void this.sendRequest(message);
|
|
153
|
+
return;
|
|
106
154
|
}
|
|
107
|
-
|
|
155
|
+
if (!this.isConnected()) throw new WebSocketError("WebSocket is not connected", "NOT_CONNECTED");
|
|
108
156
|
try {
|
|
109
|
-
this.ws!.send(JSON.stringify(message))
|
|
157
|
+
this.ws!.send(JSON.stringify(message));
|
|
110
158
|
} catch (error) {
|
|
111
|
-
throw new MessageError(
|
|
159
|
+
throw new MessageError("Failed to send message", error as Error);
|
|
112
160
|
}
|
|
113
161
|
}
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
async sendRequest<T = any>(message: any): Promise<T> {
|
|
119
|
-
if (!this.isConnected()) {
|
|
120
|
-
throw new WebSocketError('WebSocket is not connected', 'NOT_CONNECTED')
|
|
163
|
+
async sendRequest<T = unknown>(message: unknown): Promise<T> {
|
|
164
|
+
if (this.useRestTransport) {
|
|
165
|
+
return this.sendRestRequest<T>(message);
|
|
121
166
|
}
|
|
122
|
-
|
|
167
|
+
if (!this.isConnected()) throw new WebSocketError("WebSocket is not connected", "NOT_CONNECTED");
|
|
123
168
|
return new Promise((resolve, reject) => {
|
|
124
|
-
const requestId = ++this.requestId
|
|
125
|
-
const messageWithId = { ...message, requestId }
|
|
126
|
-
|
|
127
|
-
// 设置超时计时器
|
|
169
|
+
const requestId = ++this.requestId;
|
|
170
|
+
const messageWithId = { ...(message as Record<string, unknown>), requestId };
|
|
128
171
|
const timer = setTimeout(() => {
|
|
129
|
-
this.pendingRequests.delete(requestId)
|
|
130
|
-
reject(new RequestTimeoutError(requestId))
|
|
131
|
-
}, this.config.requestTimeout)
|
|
132
|
-
|
|
133
|
-
// 存储请求信息
|
|
134
|
-
this.pendingRequests.set(requestId, { resolve, reject, timer })
|
|
135
|
-
|
|
172
|
+
this.pendingRequests.delete(requestId);
|
|
173
|
+
reject(new RequestTimeoutError(requestId));
|
|
174
|
+
}, this.config.requestTimeout);
|
|
175
|
+
this.pendingRequests.set(requestId, { resolve: resolve as (v: unknown) => void, reject, timer });
|
|
136
176
|
try {
|
|
137
|
-
this.ws!.send(JSON.stringify(messageWithId))
|
|
177
|
+
this.ws!.send(JSON.stringify(messageWithId));
|
|
138
178
|
} catch (error) {
|
|
139
|
-
this.pendingRequests.delete(requestId)
|
|
140
|
-
clearTimeout(timer)
|
|
141
|
-
reject(new MessageError(
|
|
179
|
+
this.pendingRequests.delete(requestId);
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
reject(new MessageError("Failed to send request", error as Error));
|
|
142
182
|
}
|
|
143
|
-
})
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async sendRestRequest<T>(message: unknown): Promise<T> {
|
|
187
|
+
const token = getStoredToken();
|
|
188
|
+
if (!token) throw new WebSocketError("Not authenticated", "NOT_CONNECTED");
|
|
189
|
+
const requestId = ++this.requestId;
|
|
190
|
+
const body = { ...(message as Record<string, unknown>), requestId };
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
/* handled below */
|
|
193
|
+
}, this.config.requestTimeout);
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(resolveApiUrl("/api/console/request"), {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
Authorization: `Bearer ${token}`,
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify(body),
|
|
202
|
+
signal: AbortSignal.timeout(this.config.requestTimeout),
|
|
203
|
+
});
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
const json = (await res.json()) as {
|
|
206
|
+
success?: boolean;
|
|
207
|
+
data?: T;
|
|
208
|
+
error?: string;
|
|
209
|
+
requestId?: number;
|
|
210
|
+
};
|
|
211
|
+
if (!res.ok || json.success === false) {
|
|
212
|
+
throw new WebSocketError(json.error ?? `HTTP ${res.status}`, "SERVER_ERROR");
|
|
213
|
+
}
|
|
214
|
+
return json.data as T;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
if (error instanceof WebSocketError) throw error;
|
|
218
|
+
throw new MessageError("REST request failed", error as Error);
|
|
219
|
+
}
|
|
144
220
|
}
|
|
145
221
|
|
|
146
|
-
/**
|
|
147
|
-
* 检查连接状态
|
|
148
|
-
*/
|
|
149
222
|
isConnected(): boolean {
|
|
150
|
-
|
|
223
|
+
if (this.useRestTransport) {
|
|
224
|
+
return this.state === ConnectionState.CONNECTED;
|
|
225
|
+
}
|
|
226
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
151
227
|
}
|
|
152
228
|
|
|
153
|
-
/**
|
|
154
|
-
* 获取当前状态
|
|
155
|
-
*/
|
|
156
229
|
getState(): ConnectionState {
|
|
157
|
-
return this.state
|
|
230
|
+
return this.state;
|
|
158
231
|
}
|
|
159
232
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// ============================================================================
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 获取插件配置
|
|
166
|
-
*/
|
|
167
|
-
async getConfig(pluginName: string): Promise<any> {
|
|
168
|
-
return this.sendRequest({
|
|
169
|
-
type: 'config:get',
|
|
170
|
-
pluginName
|
|
171
|
-
})
|
|
233
|
+
async getConfig(pluginName: string) {
|
|
234
|
+
return this.sendRequest<unknown>({ type: "config:get", pluginName });
|
|
172
235
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*/
|
|
177
|
-
async setConfig(pluginName: string, config: any): Promise<{ success?: boolean; reloaded?: boolean; message?: string }> {
|
|
178
|
-
return this.sendRequest({
|
|
179
|
-
type: 'config:set',
|
|
236
|
+
async setConfig(pluginName: string, config: unknown) {
|
|
237
|
+
return this.sendRequest<{ success?: boolean; reloaded?: boolean; message?: string }>({
|
|
238
|
+
type: "config:set",
|
|
180
239
|
pluginName,
|
|
181
|
-
data: config
|
|
182
|
-
})
|
|
240
|
+
data: config,
|
|
241
|
+
});
|
|
183
242
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
* 获取插件 Schema
|
|
187
|
-
*/
|
|
188
|
-
async getSchema(pluginName: string): Promise<any> {
|
|
189
|
-
return this.sendRequest({
|
|
190
|
-
type: 'schema:get',
|
|
191
|
-
pluginName
|
|
192
|
-
})
|
|
243
|
+
async getSchema(pluginName: string) {
|
|
244
|
+
return this.sendRequest<unknown>({ type: "schema:get", pluginName });
|
|
193
245
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
* 获取所有配置
|
|
197
|
-
*/
|
|
198
|
-
async getAllConfigs(): Promise<Record<string, any>> {
|
|
199
|
-
return this.sendRequest({
|
|
200
|
-
type: 'config:get-all'
|
|
201
|
-
})
|
|
246
|
+
async getAllConfigs() {
|
|
247
|
+
return this.sendRequest<Record<string, unknown>>({ type: "config:get-all" });
|
|
202
248
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
* 获取所有 Schema
|
|
206
|
-
*/
|
|
207
|
-
async getAllSchemas(): Promise<Record<string, any>> {
|
|
208
|
-
return this.sendRequest({
|
|
209
|
-
type: 'schema:get-all'
|
|
210
|
-
})
|
|
249
|
+
async getAllSchemas() {
|
|
250
|
+
return this.sendRequest<Record<string, unknown>>({ type: "schema:get-all" });
|
|
211
251
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// 配置文件原始 YAML 读写
|
|
215
|
-
// ============================================================================
|
|
216
|
-
|
|
217
|
-
async getConfigYaml(): Promise<{ yaml: string; pluginKeys: string[] }> {
|
|
218
|
-
return this.sendRequest({ type: 'config:get-yaml' })
|
|
252
|
+
async getConfigYaml() {
|
|
253
|
+
return this.sendRequest<{ yaml: string; pluginKeys: string[] }>({ type: "config:get-yaml" });
|
|
219
254
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return this.sendRequest({ type: 'config:save-yaml', yaml })
|
|
255
|
+
async saveConfigYaml(yaml: string) {
|
|
256
|
+
return this.sendRequest<{ success: boolean; message?: string }>({ type: "config:save-yaml", yaml });
|
|
223
257
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// 环境变量文件管理
|
|
227
|
-
// ============================================================================
|
|
228
|
-
|
|
229
|
-
async getEnvList(): Promise<{ files: Array<{ name: string; exists: boolean }> }> {
|
|
230
|
-
return this.sendRequest({ type: 'env:list' })
|
|
258
|
+
async getEnvList() {
|
|
259
|
+
return this.sendRequest<{ files: Array<{ name: string; exists: boolean }> }>({ type: "env:list" });
|
|
231
260
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return this.sendRequest({ type: 'env:get', filename })
|
|
261
|
+
async getEnvFile(filename: string) {
|
|
262
|
+
return this.sendRequest<{ content: string }>({ type: "env:get", filename });
|
|
235
263
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return this.sendRequest({ type: 'env:save', filename, content })
|
|
264
|
+
async saveEnvFile(filename: string, content: string) {
|
|
265
|
+
return this.sendRequest<{ success: boolean; message?: string }>({ type: "env:save", filename, content });
|
|
239
266
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// 文件管理
|
|
243
|
-
// ============================================================================
|
|
244
|
-
|
|
245
|
-
async getFileTree(): Promise<{ tree: FileTreeNode[] }> {
|
|
246
|
-
return this.sendRequest({ type: 'files:tree' })
|
|
267
|
+
async getFileTree() {
|
|
268
|
+
return this.sendRequest<{ tree: import("./types").FileTreeNode[] }>({ type: "files:tree" });
|
|
247
269
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return this.sendRequest({ type: 'files:read', filePath })
|
|
270
|
+
async readFile(filePath: string) {
|
|
271
|
+
return this.sendRequest<{ content: string; size: number }>({ type: "files:read", filePath });
|
|
251
272
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return this.sendRequest({ type: 'files:save', filePath, content })
|
|
273
|
+
async saveFile(filePath: string, content: string) {
|
|
274
|
+
return this.sendRequest<{ success: boolean; message?: string }>({ type: "files:save", filePath, content });
|
|
255
275
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// 数据库管理
|
|
259
|
-
// ============================================================================
|
|
260
|
-
|
|
261
|
-
async getDbInfo(): Promise<DatabaseInfo> {
|
|
262
|
-
return this.sendRequest({ type: 'db:info' })
|
|
276
|
+
async getDbInfo() {
|
|
277
|
+
return this.sendRequest<import("./types").DatabaseInfo>({ type: "db:info" });
|
|
263
278
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return this.sendRequest({ type: 'db:tables' })
|
|
279
|
+
async getDbTables() {
|
|
280
|
+
return this.sendRequest<{ tables: import("./types").TableInfo[] }>({ type: "db:tables" });
|
|
267
281
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return this.sendRequest({ type: 'db:select', table, page, pageSize, where })
|
|
282
|
+
async dbSelect(table: string, page?: number, pageSize?: number, where?: unknown) {
|
|
283
|
+
return this.sendRequest<import("./types").SelectResult>({ type: "db:select", table, page, pageSize, where });
|
|
271
284
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return this.sendRequest({ type: 'db:insert', table, row })
|
|
285
|
+
async dbInsert(table: string, row: unknown) {
|
|
286
|
+
return this.sendRequest<{ success: boolean }>({ type: "db:insert", table, row });
|
|
275
287
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return this.sendRequest({ type: 'db:update', table, row, where })
|
|
288
|
+
async dbUpdate(table: string, row: unknown, where: unknown) {
|
|
289
|
+
return this.sendRequest<{ success: boolean; affected: number }>({ type: "db:update", table, row, where });
|
|
279
290
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return this.sendRequest({ type: 'db:delete', table, where })
|
|
291
|
+
async dbDelete(table: string, where: unknown) {
|
|
292
|
+
return this.sendRequest<{ success: boolean; deleted: number }>({ type: "db:delete", table, where });
|
|
283
293
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return this.sendRequest({ type: 'db:drop-table', table })
|
|
294
|
+
async dbDropTable(table: string) {
|
|
295
|
+
return this.sendRequest<{ success: boolean }>({ type: "db:drop-table", table });
|
|
287
296
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return this.sendRequest({ type: 'db:kv:get', table, key })
|
|
297
|
+
async kvGet(table: string, key: string) {
|
|
298
|
+
return this.sendRequest<{ key: string; value: unknown }>({ type: "db:kv:get", table, key });
|
|
291
299
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return this.sendRequest({ type: 'db:kv:set', table, key, value, ttl })
|
|
300
|
+
async kvSet(table: string, key: string, value: unknown, ttl?: number) {
|
|
301
|
+
return this.sendRequest<{ success: boolean }>({ type: "db:kv:set", table, key, value, ttl });
|
|
295
302
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return this.sendRequest({ type: 'db:kv:delete', table, key })
|
|
303
|
+
async kvDelete(table: string, key: string) {
|
|
304
|
+
return this.sendRequest<{ success: boolean }>({ type: "db:kv:delete", table, key });
|
|
299
305
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return this.sendRequest({ type: 'db:kv:entries', table })
|
|
306
|
+
async kvGetEntries(table: string) {
|
|
307
|
+
return this.sendRequest<{ entries: import("./types").KvEntry[] }>({ type: "db:kv:entries", table });
|
|
303
308
|
}
|
|
304
309
|
|
|
305
|
-
// ============================================================================
|
|
306
|
-
// 私有方法
|
|
307
|
-
// ============================================================================
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* 构建 WebSocket URL
|
|
311
|
-
*/
|
|
312
310
|
private buildWebSocketUrl(customUrl?: string): string {
|
|
313
|
-
if (customUrl)
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
318
|
-
const host = window.location.host
|
|
319
|
-
return `${protocol}//${host}/server`
|
|
311
|
+
if (customUrl) return customUrl;
|
|
312
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
313
|
+
return `${protocol}//${window.location.host}/server`;
|
|
320
314
|
}
|
|
321
315
|
|
|
322
|
-
/**
|
|
323
|
-
* 设置连接状态
|
|
324
|
-
*/
|
|
325
316
|
private setState(newState: ConnectionState): void {
|
|
326
|
-
|
|
327
|
-
this.state = newState
|
|
328
|
-
|
|
329
|
-
console.debug(`[WebSocket] State changed: ${oldState} -> ${newState}`)
|
|
317
|
+
this.state = newState;
|
|
330
318
|
}
|
|
331
319
|
|
|
332
|
-
/**
|
|
333
|
-
* 绑定事件处理器
|
|
334
|
-
*/
|
|
335
320
|
private attachEventHandlers(): void {
|
|
336
|
-
if (!this.ws) return
|
|
337
|
-
|
|
338
|
-
this.ws.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
this.ws.onmessage = (event) => {
|
|
343
|
-
this.handleMessage(event)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
this.ws.onclose = (event) => {
|
|
347
|
-
this.handleConnectionClose(event)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
this.ws.onerror = (event) => {
|
|
351
|
-
this.handleConnectionError(new ConnectionError('WebSocket error', event as any))
|
|
352
|
-
}
|
|
321
|
+
if (!this.ws) return;
|
|
322
|
+
this.ws.onopen = () => this.handleConnectionOpen();
|
|
323
|
+
this.ws.onmessage = (event) => this.handleMessage(event);
|
|
324
|
+
this.ws.onclose = () => this.handleConnectionClose();
|
|
325
|
+
this.ws.onerror = (event) =>
|
|
326
|
+
this.handleConnectionError(new ConnectionError("WebSocket error", event as unknown as Error));
|
|
353
327
|
}
|
|
354
328
|
|
|
355
|
-
/**
|
|
356
|
-
* 处理连接打开
|
|
357
|
-
*/
|
|
358
329
|
private handleConnectionOpen(): void {
|
|
359
|
-
this.setState(ConnectionState.CONNECTED)
|
|
360
|
-
this.reconnectAttempts = 0
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
store.dispatch(setConnected(true))
|
|
364
|
-
|
|
365
|
-
// 初始化数据
|
|
366
|
-
this.initializeData()
|
|
367
|
-
|
|
368
|
-
// 触发回调
|
|
369
|
-
this.callbacks.onConnect?.()
|
|
330
|
+
this.setState(ConnectionState.CONNECTED);
|
|
331
|
+
this.reconnectAttempts = 0;
|
|
332
|
+
this.notifyConnection(true);
|
|
333
|
+
this.callbacks.onConnect?.();
|
|
370
334
|
}
|
|
371
335
|
|
|
372
|
-
/**
|
|
373
|
-
* 处理消息接收
|
|
374
|
-
*/
|
|
375
336
|
private handleMessage(event: MessageEvent): void {
|
|
376
337
|
try {
|
|
377
|
-
const message
|
|
378
|
-
|
|
379
|
-
// 处理请求响应
|
|
338
|
+
const message = JSON.parse(event.data) as WebSocketMessage;
|
|
380
339
|
if (message.requestId && this.pendingRequests.has(message.requestId)) {
|
|
381
|
-
this.handleRequestResponse(message)
|
|
382
|
-
return
|
|
340
|
+
this.handleRequestResponse(message);
|
|
341
|
+
return;
|
|
383
342
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this.callbacks.onMessage?.(message)
|
|
388
|
-
|
|
343
|
+
void applyConsoleEvent(message);
|
|
344
|
+
this.handleBroadcast(message);
|
|
345
|
+
this.callbacks.onMessage?.(message);
|
|
389
346
|
} catch (error) {
|
|
390
|
-
console.error(
|
|
347
|
+
console.error("[WebSocket] Message parsing error:", error);
|
|
391
348
|
}
|
|
392
349
|
}
|
|
393
350
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (!pendingRequest) {
|
|
402
|
-
return
|
|
351
|
+
private handleBroadcast(message: WebSocketMessage): void {
|
|
352
|
+
const t = message.type;
|
|
353
|
+
if (t === "bot:request" || t === "bot:notice" || t === "bot:message") {
|
|
354
|
+
if (typeof window !== "undefined") {
|
|
355
|
+
window.dispatchEvent(new CustomEvent("zhin-console-bot-push", { detail: message }));
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
403
358
|
}
|
|
359
|
+
if (t === "hmr:reload") {
|
|
360
|
+
console.info(`[HMR] File changed, reloading...`);
|
|
361
|
+
window.location.reload();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (t === "system:restarting") {
|
|
365
|
+
console.info("[System] Server restarting, will reload shortly...");
|
|
366
|
+
setTimeout(() => window.location.reload(), 3000);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
404
370
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
371
|
+
private handleRequestResponse(message: WebSocketMessage): void {
|
|
372
|
+
const { requestId } = message;
|
|
373
|
+
const pending = this.pendingRequests.get(requestId!);
|
|
374
|
+
if (!pending) return;
|
|
375
|
+
this.pendingRequests.delete(requestId!);
|
|
376
|
+
clearTimeout(pending.timer);
|
|
408
377
|
if (message.error) {
|
|
409
|
-
|
|
378
|
+
pending.reject(new WebSocketError(message.error, "SERVER_ERROR"));
|
|
410
379
|
} else {
|
|
411
|
-
|
|
380
|
+
pending.resolve(message.data);
|
|
412
381
|
}
|
|
413
382
|
}
|
|
414
383
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (this.state === ConnectionState.DISCONNECTED) {
|
|
423
|
-
// 主动断开,不重连
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
this.setState(ConnectionState.RECONNECTING)
|
|
428
|
-
this.callbacks.onDisconnect?.()
|
|
429
|
-
this.scheduleReconnect()
|
|
384
|
+
private handleConnectionClose(): void {
|
|
385
|
+
this.ws = null;
|
|
386
|
+
this.notifyConnection(false);
|
|
387
|
+
if (this.state === ConnectionState.DISCONNECTED) return;
|
|
388
|
+
this.setState(ConnectionState.RECONNECTING);
|
|
389
|
+
this.callbacks.onDisconnect?.();
|
|
390
|
+
this.scheduleReconnect();
|
|
430
391
|
}
|
|
431
392
|
|
|
432
|
-
/**
|
|
433
|
-
* 处理连接错误
|
|
434
|
-
*/
|
|
435
393
|
private handleConnectionError(error: Error): void {
|
|
436
|
-
this.clearReconnectTimer()
|
|
437
|
-
this.setState(ConnectionState.ERROR)
|
|
438
|
-
console.error(
|
|
439
|
-
this.callbacks.onError?.(error as
|
|
394
|
+
this.clearReconnectTimer();
|
|
395
|
+
this.setState(ConnectionState.ERROR);
|
|
396
|
+
console.error("[Console transport] Connection error:", error);
|
|
397
|
+
this.callbacks.onError?.(error as unknown as Event);
|
|
440
398
|
}
|
|
441
399
|
|
|
442
|
-
/**
|
|
443
|
-
* 安排重连
|
|
444
|
-
*/
|
|
445
400
|
private scheduleReconnect(): void {
|
|
446
401
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return
|
|
402
|
+
this.setState(ConnectionState.ERROR);
|
|
403
|
+
return;
|
|
450
404
|
}
|
|
451
|
-
|
|
452
|
-
this.reconnectAttempts
|
|
453
|
-
const delay = this.config.reconnectInterval * this.reconnectAttempts
|
|
454
|
-
|
|
455
|
-
console.debug(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`)
|
|
456
|
-
|
|
405
|
+
this.reconnectAttempts++;
|
|
406
|
+
const delay = this.config.reconnectInterval * this.reconnectAttempts;
|
|
457
407
|
this.reconnectTimer = setTimeout(() => {
|
|
458
|
-
if (this.state === ConnectionState.RECONNECTING)
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
}, delay)
|
|
408
|
+
if (this.state === ConnectionState.RECONNECTING) this.connect();
|
|
409
|
+
}, delay);
|
|
462
410
|
}
|
|
463
411
|
|
|
464
|
-
/**
|
|
465
|
-
* 清除重连计时器
|
|
466
|
-
*/
|
|
467
412
|
private clearReconnectTimer(): void {
|
|
468
413
|
if (this.reconnectTimer) {
|
|
469
|
-
clearTimeout(this.reconnectTimer)
|
|
470
|
-
this.reconnectTimer = null
|
|
414
|
+
clearTimeout(this.reconnectTimer);
|
|
415
|
+
this.reconnectTimer = null;
|
|
471
416
|
}
|
|
472
417
|
}
|
|
473
418
|
|
|
474
|
-
/**
|
|
475
|
-
* 清除所有待处理请求
|
|
476
|
-
*/
|
|
477
419
|
private clearPendingRequests(): void {
|
|
478
|
-
for (const [
|
|
479
|
-
clearTimeout(timer)
|
|
480
|
-
reject(new WebSocketError(
|
|
481
|
-
}
|
|
482
|
-
this.pendingRequests.clear()
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* 初始化数据
|
|
487
|
-
*/
|
|
488
|
-
private async initializeData(): Promise<void> {
|
|
489
|
-
try {
|
|
490
|
-
// 并行获取所有配置和 Schema
|
|
491
|
-
const [configs, schemas] = await Promise.all([
|
|
492
|
-
this.getAllConfigs().catch(error => {
|
|
493
|
-
console.warn('[WebSocket] Failed to load configs:', error)
|
|
494
|
-
return {}
|
|
495
|
-
}),
|
|
496
|
-
this.getAllSchemas().catch(error => {
|
|
497
|
-
console.warn('[WebSocket] Failed to load schemas:', error)
|
|
498
|
-
return {}
|
|
499
|
-
})
|
|
500
|
-
])
|
|
501
|
-
|
|
502
|
-
// 更新 Redux 状态
|
|
503
|
-
store.dispatch(updateConfigs(configs))
|
|
504
|
-
store.dispatch(updateSchemas(schemas))
|
|
505
|
-
|
|
506
|
-
} catch (error) {
|
|
507
|
-
console.error('[WebSocket] Data initialization failed:', error)
|
|
420
|
+
for (const [, { reject, timer }] of this.pendingRequests) {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
reject(new WebSocketError("Connection closed", "CONNECTION_CLOSED"));
|
|
508
423
|
}
|
|
424
|
+
this.pendingRequests.clear();
|
|
509
425
|
}
|
|
510
|
-
}
|
|
426
|
+
}
|