@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.
Files changed (116) hide show
  1. package/client/app.ts +228 -0
  2. package/client/bootstrap/loadConsoleEntries.test.ts +32 -0
  3. package/client/bootstrap/loadConsoleEntries.ts +149 -0
  4. package/client/console-utils/cn.ts +6 -0
  5. package/client/console-utils/remoteApi.ts +48 -0
  6. package/client/index.ts +61 -14
  7. package/client/persistence/idb-store.ts +76 -0
  8. package/client/runtime/index.ts +12 -0
  9. package/client/store/createRegistryStore.ts +46 -0
  10. package/client/websocket/hooks.ts +218 -510
  11. package/client/websocket/index.ts +6 -43
  12. package/client/websocket/instance.ts +10 -32
  13. package/client/websocket/manager.ts +301 -385
  14. package/client/websocket/remote-settings.ts +25 -0
  15. package/client/websocket/types.ts +58 -222
  16. package/dist/app.d.ts +95 -0
  17. package/dist/app.d.ts.map +1 -0
  18. package/dist/app.js +147 -0
  19. package/dist/app.js.map +1 -0
  20. package/dist/bootstrap/loadConsoleEntries.d.ts +26 -0
  21. package/dist/bootstrap/loadConsoleEntries.d.ts.map +1 -0
  22. package/dist/bootstrap/loadConsoleEntries.js +111 -0
  23. package/dist/bootstrap/loadConsoleEntries.js.map +1 -0
  24. package/dist/bootstrap/loadConsoleEntries.test.d.ts +2 -0
  25. package/dist/bootstrap/loadConsoleEntries.test.d.ts.map +1 -0
  26. package/dist/bootstrap/loadConsoleEntries.test.js +27 -0
  27. package/dist/bootstrap/loadConsoleEntries.test.js.map +1 -0
  28. package/dist/console-utils/cn.d.ts +3 -0
  29. package/dist/console-utils/cn.d.ts.map +1 -0
  30. package/dist/console-utils/cn.js +6 -0
  31. package/dist/console-utils/cn.js.map +1 -0
  32. package/dist/console-utils/remoteApi.d.ts +7 -0
  33. package/dist/console-utils/remoteApi.d.ts.map +1 -0
  34. package/dist/console-utils/remoteApi.js +41 -0
  35. package/dist/console-utils/remoteApi.js.map +1 -0
  36. package/dist/index.d.ts +11 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +13 -14
  39. package/dist/index.js.map +1 -1
  40. package/dist/persistence/idb-store.d.ts +15 -0
  41. package/dist/persistence/idb-store.d.ts.map +1 -0
  42. package/dist/persistence/idb-store.js +61 -0
  43. package/dist/persistence/idb-store.js.map +1 -0
  44. package/dist/runtime/index.d.ts +6 -0
  45. package/dist/runtime/index.d.ts.map +1 -0
  46. package/dist/runtime/index.js +10 -0
  47. package/dist/runtime/index.js.map +1 -0
  48. package/dist/store/createRegistryStore.d.ts +9 -0
  49. package/dist/store/createRegistryStore.d.ts.map +1 -0
  50. package/dist/store/createRegistryStore.js +38 -0
  51. package/dist/store/createRegistryStore.js.map +1 -0
  52. package/dist/websocket/hooks.d.ts +35 -58
  53. package/dist/websocket/hooks.d.ts.map +1 -1
  54. package/dist/websocket/hooks.js +134 -410
  55. package/dist/websocket/hooks.js.map +1 -1
  56. package/dist/websocket/index.d.ts +5 -10
  57. package/dist/websocket/index.d.ts.map +1 -1
  58. package/dist/websocket/index.js +5 -25
  59. package/dist/websocket/index.js.map +1 -1
  60. package/dist/websocket/instance.d.ts +1 -14
  61. package/dist/websocket/instance.d.ts.map +1 -1
  62. package/dist/websocket/instance.js +2 -20
  63. package/dist/websocket/instance.js.map +1 -1
  64. package/dist/websocket/manager.d.ts +28 -93
  65. package/dist/websocket/manager.d.ts.map +1 -1
  66. package/dist/websocket/manager.js +213 -221
  67. package/dist/websocket/manager.js.map +1 -1
  68. package/dist/websocket/remote-settings.d.ts +5 -0
  69. package/dist/websocket/remote-settings.d.ts.map +1 -0
  70. package/dist/websocket/remote-settings.js +23 -0
  71. package/dist/websocket/remote-settings.js.map +1 -0
  72. package/dist/websocket/types.d.ts +35 -122
  73. package/dist/websocket/types.d.ts.map +1 -1
  74. package/dist/websocket/types.js +4 -13
  75. package/dist/websocket/types.js.map +1 -1
  76. package/package.json +13 -11
  77. package/client/router/index.tsx +0 -54
  78. package/client/store/index.ts +0 -132
  79. package/client/store/reducers/config.ts +0 -135
  80. package/client/store/reducers/index.ts +0 -19
  81. package/client/store/reducers/route.ts +0 -126
  82. package/client/store/reducers/script.ts +0 -109
  83. package/client/store/reducers/ui.ts +0 -31
  84. package/client/websocket/messageHandler.ts +0 -208
  85. package/dist/router/index.d.ts +0 -25
  86. package/dist/router/index.d.ts.map +0 -1
  87. package/dist/router/index.js +0 -48
  88. package/dist/router/index.js.map +0 -1
  89. package/dist/store/index.d.ts +0 -19
  90. package/dist/store/index.d.ts.map +0 -1
  91. package/dist/store/index.js +0 -67
  92. package/dist/store/index.js.map +0 -1
  93. package/dist/store/reducers/config.d.ts +0 -54
  94. package/dist/store/reducers/config.d.ts.map +0 -1
  95. package/dist/store/reducers/config.js +0 -78
  96. package/dist/store/reducers/config.js.map +0 -1
  97. package/dist/store/reducers/index.d.ts +0 -13
  98. package/dist/store/reducers/index.d.ts.map +0 -1
  99. package/dist/store/reducers/index.js +0 -11
  100. package/dist/store/reducers/index.js.map +0 -1
  101. package/dist/store/reducers/route.d.ts +0 -30
  102. package/dist/store/reducers/route.d.ts.map +0 -1
  103. package/dist/store/reducers/route.js +0 -85
  104. package/dist/store/reducers/route.js.map +0 -1
  105. package/dist/store/reducers/script.d.ts +0 -13
  106. package/dist/store/reducers/script.d.ts.map +0 -1
  107. package/dist/store/reducers/script.js +0 -78
  108. package/dist/store/reducers/script.js.map +0 -1
  109. package/dist/store/reducers/ui.d.ts +0 -8
  110. package/dist/store/reducers/ui.d.ts.map +0 -1
  111. package/dist/store/reducers/ui.js +0 -23
  112. package/dist/store/reducers/ui.js.map +0 -1
  113. package/dist/websocket/messageHandler.d.ts +0 -58
  114. package/dist/websocket/messageHandler.d.ts.map +0 -1
  115. package/dist/websocket/messageHandler.js +0 -174
  116. 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
- FileTreeNode,
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 './types'
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 ws: WebSocket | null = null
32
- private config: Required<WebSocketConfig>
33
- private callbacks: WebSocketCallbacks
34
- private state: ConnectionState = ConnectionState.DISCONNECTED
35
-
36
- // 重连相关
37
- private reconnectAttempts = 0
38
- private reconnectTimer: NodeJS.Timeout | null = null
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
- // 公共 API
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.state === ConnectionState.CONNECTED || this.state === ConnectionState.CONNECTING) {
71
- return
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
- this.setState(ConnectionState.CONNECTING)
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
- this.ws = new WebSocket(this.config.url)
78
- this.attachEventHandlers()
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
- this.handleConnectionError(new ConnectionError('Failed to create WebSocket', error as Error))
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.setState(ConnectionState.DISCONNECTED)
97
- store.dispatch(setConnected(false))
146
+ this.setState(ConnectionState.DISCONNECTED);
147
+ this.notifyConnection(false);
98
148
  }
99
149
 
100
- /**
101
- * 发送普通消息
102
- */
103
- send(message: any): void {
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('Failed to send message', error as Error)
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('Failed to send request', error as Error))
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
- return this.ws?.readyState === WebSocket.OPEN
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
- // 配置管理 API
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
- async saveConfigYaml(yaml: string): Promise<{ success: boolean; message?: string }> {
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
- async getEnvFile(filename: string): Promise<{ content: string }> {
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
- async saveEnvFile(filename: string, content: string): Promise<{ success: boolean; message?: string }> {
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
- async readFile(filePath: string): Promise<{ content: string; size: number }> {
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
- async saveFile(filePath: string, content: string): Promise<{ success: boolean; message?: string }> {
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
- async getDbTables(): Promise<{ tables: TableInfo[] }> {
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
- async dbSelect(table: string, page?: number, pageSize?: number, where?: any): Promise<SelectResult> {
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
- async dbInsert(table: string, row: any): Promise<{ success: boolean }> {
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
- async dbUpdate(table: string, row: any, where: any): Promise<{ success: boolean; affected: number }> {
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
- async dbDelete(table: string, where: any): Promise<{ success: boolean; deleted: number }> {
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
- async dbDropTable(table: string): Promise<{ success: boolean }> {
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
- async kvGet(table: string, key: string): Promise<{ key: string; value: any }> {
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
- async kvSet(table: string, key: string, value: any, ttl?: number): Promise<{ success: boolean }> {
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
- async kvDelete(table: string, key: string): Promise<{ success: boolean }> {
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
- async kvGetEntries(table: string): Promise<{ entries: KvEntry[] }> {
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
- return customUrl
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
- const oldState = this.state
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.onopen = () => {
339
- this.handleConnectionOpen()
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
- // 更新 Redux 状态
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: WebSocketMessage = JSON.parse(event.data)
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
- MessageHandler.handle(message)
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('[WebSocket] Message parsing error:', error)
347
+ console.error("[WebSocket] Message parsing error:", error);
391
348
  }
392
349
  }
393
350
 
394
- /**
395
- * 处理请求响应
396
- */
397
- private handleRequestResponse(message: any): void {
398
- const { requestId } = message
399
- const pendingRequest = this.pendingRequests.get(requestId)
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
- this.pendingRequests.delete(requestId)
406
- clearTimeout(pendingRequest.timer)
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
- pendingRequest.reject(new WebSocketError(message.error, 'SERVER_ERROR'))
378
+ pending.reject(new WebSocketError(message.error, "SERVER_ERROR"));
410
379
  } else {
411
- pendingRequest.resolve(message.data)
380
+ pending.resolve(message.data);
412
381
  }
413
382
  }
414
383
 
415
- /**
416
- * 处理连接关闭
417
- */
418
- private handleConnectionClose(event: CloseEvent): void {
419
- this.ws = null
420
- store.dispatch(setConnected(false))
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('[WebSocket] Connection error:', error)
439
- this.callbacks.onError?.(error as any)
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
- console.error('[WebSocket] Max reconnect attempts reached')
448
- this.setState(ConnectionState.ERROR)
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
- this.connect()
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 [requestId, { reject, timer }] of this.pendingRequests) {
479
- clearTimeout(timer)
480
- reject(new WebSocketError('Connection closed', 'CONNECTION_CLOSED'))
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
+ }