@hangox/mg-cli 1.0.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/.eslintrc.cjs +26 -0
- package/CLAUDE.md +43 -0
- package/bin/mg-cli.js +4 -0
- package/package.json +51 -0
- package/src/cli/client.ts +266 -0
- package/src/cli/commands/execute-code.ts +59 -0
- package/src/cli/commands/export-image.ts +193 -0
- package/src/cli/commands/get-all-nodes.ts +81 -0
- package/src/cli/commands/get-all-pages.ts +118 -0
- package/src/cli/commands/get-node-by-id.ts +83 -0
- package/src/cli/commands/get-node-by-link.ts +105 -0
- package/src/cli/commands/server.ts +130 -0
- package/src/cli/index.ts +33 -0
- package/src/index.ts +9 -0
- package/src/server/connection-manager.ts +211 -0
- package/src/server/daemon-runner.ts +22 -0
- package/src/server/daemon.ts +211 -0
- package/src/server/index.ts +8 -0
- package/src/server/logger.ts +117 -0
- package/src/server/request-handler.ts +192 -0
- package/src/server/websocket-server.ts +297 -0
- package/src/shared/constants.ts +90 -0
- package/src/shared/errors.ts +131 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/types.ts +227 -0
- package/src/shared/utils.ts +352 -0
- package/tests/unit/shared/constants.test.ts +66 -0
- package/tests/unit/shared/errors.test.ts +82 -0
- package/tests/unit/shared/utils.test.ts +208 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +33 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求处理器
|
|
3
|
+
* 处理 Consumer 请求,转发给 Provider
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManagedWebSocket } from './connection-manager.js'
|
|
7
|
+
import { ConnectionManager } from './connection-manager.js'
|
|
8
|
+
import { Logger } from './logger.js'
|
|
9
|
+
import { MessageType, REQUEST_TIMEOUT } from '../shared/constants.js'
|
|
10
|
+
import { ErrorCode, MGError } from '../shared/errors.js'
|
|
11
|
+
import type { RequestMessage, ResponseMessage } from '../shared/types.js'
|
|
12
|
+
import { generateId } from '../shared/utils.js'
|
|
13
|
+
|
|
14
|
+
/** 待处理的请求 */
|
|
15
|
+
interface PendingRequest {
|
|
16
|
+
/** 请求 ID */
|
|
17
|
+
id: string
|
|
18
|
+
/** Consumer WebSocket */
|
|
19
|
+
consumer: ManagedWebSocket
|
|
20
|
+
/** 超时定时器 */
|
|
21
|
+
timer: NodeJS.Timeout
|
|
22
|
+
/** 请求时间 */
|
|
23
|
+
timestamp: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 请求处理器
|
|
28
|
+
*/
|
|
29
|
+
export class RequestHandler {
|
|
30
|
+
private logger: Logger
|
|
31
|
+
private connectionManager: ConnectionManager
|
|
32
|
+
|
|
33
|
+
/** 待处理的请求 */
|
|
34
|
+
private pendingRequests = new Map<string, PendingRequest>()
|
|
35
|
+
|
|
36
|
+
constructor(connectionManager: ConnectionManager, logger: Logger) {
|
|
37
|
+
this.connectionManager = connectionManager
|
|
38
|
+
this.logger = logger
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 处理 Consumer 请求
|
|
43
|
+
*/
|
|
44
|
+
async handleRequest(consumer: ManagedWebSocket, message: RequestMessage): Promise<void> {
|
|
45
|
+
const requestId = message.id || generateId()
|
|
46
|
+
const { type, pageUrl, params } = message
|
|
47
|
+
|
|
48
|
+
this.logger.info(`收到请求: ${type} (${requestId})`, { pageUrl })
|
|
49
|
+
|
|
50
|
+
// 查找目标 Provider
|
|
51
|
+
let provider: ManagedWebSocket | undefined
|
|
52
|
+
|
|
53
|
+
if (pageUrl) {
|
|
54
|
+
provider = this.connectionManager.findProviderByPageUrl(pageUrl)
|
|
55
|
+
if (!provider) {
|
|
56
|
+
this.sendError(consumer, requestId, ErrorCode.PAGE_NOT_FOUND, `未找到页面: ${pageUrl}`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// 没有指定 pageUrl,使用第一个可用的 Provider
|
|
61
|
+
provider = this.connectionManager.getFirstProvider()
|
|
62
|
+
if (!provider) {
|
|
63
|
+
this.sendError(consumer, requestId, ErrorCode.NO_PAGE_CONNECTED, '没有页面连接到 Server')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 创建超时定时器
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
this.handleTimeout(requestId)
|
|
71
|
+
}, REQUEST_TIMEOUT)
|
|
72
|
+
|
|
73
|
+
// 保存待处理请求
|
|
74
|
+
this.pendingRequests.set(requestId, {
|
|
75
|
+
id: requestId,
|
|
76
|
+
consumer,
|
|
77
|
+
timer,
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// 转发请求给 Provider
|
|
82
|
+
const forwardMessage: RequestMessage = {
|
|
83
|
+
id: requestId,
|
|
84
|
+
type,
|
|
85
|
+
pageUrl: pageUrl || provider.connectionInfo.pageUrl,
|
|
86
|
+
params,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
provider.send(JSON.stringify(forwardMessage))
|
|
92
|
+
this.logger.info(`请求转发: ${type} -> ${provider.connectionInfo.pageUrl}`)
|
|
93
|
+
} catch {
|
|
94
|
+
this.cleanupRequest(requestId)
|
|
95
|
+
this.sendError(consumer, requestId, ErrorCode.CONNECTION_FAILED, '转发请求失败')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 处理 Provider 响应
|
|
101
|
+
*/
|
|
102
|
+
handleResponse(response: ResponseMessage): void {
|
|
103
|
+
const { id } = response
|
|
104
|
+
const pending = this.pendingRequests.get(id)
|
|
105
|
+
|
|
106
|
+
if (!pending) {
|
|
107
|
+
this.logger.warn(`收到未知请求的响应: ${id}`)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.cleanupRequest(id)
|
|
112
|
+
|
|
113
|
+
// 转发响应给 Consumer
|
|
114
|
+
try {
|
|
115
|
+
pending.consumer.send(JSON.stringify(response))
|
|
116
|
+
this.logger.info(
|
|
117
|
+
`响应返回: ${id} (${response.success ? '成功' : '失败'})`
|
|
118
|
+
)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.logger.error(`响应转发失败: ${id}`, error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 处理请求超时
|
|
126
|
+
*/
|
|
127
|
+
private handleTimeout(requestId: string): void {
|
|
128
|
+
const pending = this.pendingRequests.get(requestId)
|
|
129
|
+
if (!pending) return
|
|
130
|
+
|
|
131
|
+
this.logger.warn(`请求超时: ${requestId}`)
|
|
132
|
+
this.cleanupRequest(requestId)
|
|
133
|
+
|
|
134
|
+
this.sendError(pending.consumer, requestId, ErrorCode.REQUEST_TIMEOUT, '请求超时')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 清理请求
|
|
139
|
+
*/
|
|
140
|
+
private cleanupRequest(requestId: string): void {
|
|
141
|
+
const pending = this.pendingRequests.get(requestId)
|
|
142
|
+
if (pending) {
|
|
143
|
+
clearTimeout(pending.timer)
|
|
144
|
+
this.pendingRequests.delete(requestId)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 发送错误响应
|
|
150
|
+
*/
|
|
151
|
+
private sendError(
|
|
152
|
+
consumer: ManagedWebSocket,
|
|
153
|
+
requestId: string,
|
|
154
|
+
code: ErrorCode,
|
|
155
|
+
message: string
|
|
156
|
+
): void {
|
|
157
|
+
const error = new MGError(code, message)
|
|
158
|
+
const response: ResponseMessage = {
|
|
159
|
+
id: requestId,
|
|
160
|
+
type: MessageType.ERROR,
|
|
161
|
+
success: false,
|
|
162
|
+
data: null,
|
|
163
|
+
error: error.toJSON(),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
consumer.send(JSON.stringify(response))
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.logger.error(`发送错误响应失败: ${requestId}`, err)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 清理特定连接的所有待处理请求
|
|
175
|
+
*/
|
|
176
|
+
cleanupConnectionRequests(connectionId: string): void {
|
|
177
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
178
|
+
if (pending.consumer.connectionId === connectionId) {
|
|
179
|
+
this.cleanupRequest(requestId)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 清理所有待处理请求
|
|
186
|
+
*/
|
|
187
|
+
cleanupAll(): void {
|
|
188
|
+
for (const [requestId] of this.pendingRequests) {
|
|
189
|
+
this.cleanupRequest(requestId)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket 服务器
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws'
|
|
6
|
+
import type { IncomingMessage } from 'node:http'
|
|
7
|
+
import {
|
|
8
|
+
ConnectionType,
|
|
9
|
+
MessageType,
|
|
10
|
+
DEFAULT_PORT,
|
|
11
|
+
PORT_RANGE_START,
|
|
12
|
+
PORT_RANGE_END,
|
|
13
|
+
HEARTBEAT_INTERVAL,
|
|
14
|
+
} from '../shared/constants.js'
|
|
15
|
+
import { ErrorCode, MGError } from '../shared/errors.js'
|
|
16
|
+
import type { BaseMessage, RegisterMessage, ResponseMessage } from '../shared/types.js'
|
|
17
|
+
import { Logger, createLogger } from './logger.js'
|
|
18
|
+
import { ConnectionManager, ManagedWebSocket } from './connection-manager.js'
|
|
19
|
+
import { RequestHandler } from './request-handler.js'
|
|
20
|
+
|
|
21
|
+
export interface ServerOptions {
|
|
22
|
+
/** 监听端口 */
|
|
23
|
+
port?: number
|
|
24
|
+
/** 日志选项 */
|
|
25
|
+
logger?: Logger
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* MG WebSocket 服务器
|
|
30
|
+
*/
|
|
31
|
+
export class MGServer {
|
|
32
|
+
private wss: WebSocketServer | null = null
|
|
33
|
+
private logger: Logger
|
|
34
|
+
private connectionManager: ConnectionManager
|
|
35
|
+
private requestHandler: RequestHandler
|
|
36
|
+
|
|
37
|
+
private port: number
|
|
38
|
+
private isRunning = false
|
|
39
|
+
|
|
40
|
+
constructor(options: ServerOptions = {}) {
|
|
41
|
+
this.port = options.port || DEFAULT_PORT
|
|
42
|
+
this.logger = options.logger || createLogger()
|
|
43
|
+
this.connectionManager = new ConnectionManager(this.logger)
|
|
44
|
+
this.requestHandler = new RequestHandler(this.connectionManager, this.logger)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 启动服务器
|
|
49
|
+
*/
|
|
50
|
+
async start(): Promise<number> {
|
|
51
|
+
if (this.isRunning) {
|
|
52
|
+
throw new MGError(ErrorCode.SERVER_ALREADY_RUNNING, 'Server 已在运行中')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 尝试绑定端口
|
|
56
|
+
const port = await this.findAvailablePort()
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
this.wss = new WebSocketServer({ port })
|
|
60
|
+
|
|
61
|
+
this.wss.on('listening', () => {
|
|
62
|
+
this.port = port
|
|
63
|
+
this.isRunning = true
|
|
64
|
+
this.logger.info(`Server 启动成功,监听端口: ${port}`)
|
|
65
|
+
|
|
66
|
+
// 启动心跳检查
|
|
67
|
+
this.connectionManager.startHeartbeatCheck(HEARTBEAT_INTERVAL)
|
|
68
|
+
|
|
69
|
+
resolve(port)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
this.wss.on('error', (error: NodeJS.ErrnoException) => {
|
|
73
|
+
this.logger.error('Server 错误:', error)
|
|
74
|
+
reject(error)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
this.wss.on('connection', (ws: WebSocket, request: IncomingMessage) => {
|
|
78
|
+
this.handleConnection(ws, request)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 查找可用端口
|
|
85
|
+
*/
|
|
86
|
+
private async findAvailablePort(): Promise<number> {
|
|
87
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
88
|
+
const available = await this.isPortAvailable(port)
|
|
89
|
+
if (available) {
|
|
90
|
+
return port
|
|
91
|
+
}
|
|
92
|
+
this.logger.debug(`端口 ${port} 被占用,尝试下一个`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new MGError(
|
|
96
|
+
ErrorCode.PORT_EXHAUSTED,
|
|
97
|
+
`端口 ${PORT_RANGE_START}-${PORT_RANGE_END} 均被占用`
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 检查端口是否可用
|
|
103
|
+
*/
|
|
104
|
+
private isPortAvailable(port: number): Promise<boolean> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const testServer = new WebSocketServer({ port })
|
|
107
|
+
|
|
108
|
+
testServer.on('listening', () => {
|
|
109
|
+
testServer.close()
|
|
110
|
+
resolve(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
testServer.on('error', () => {
|
|
114
|
+
resolve(false)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 处理新连接
|
|
121
|
+
*/
|
|
122
|
+
private handleConnection(ws: WebSocket, _request: IncomingMessage): void {
|
|
123
|
+
this.logger.info('新连接建立')
|
|
124
|
+
|
|
125
|
+
// 等待注册消息
|
|
126
|
+
const registerTimeout = setTimeout(() => {
|
|
127
|
+
this.logger.warn('连接注册超时,关闭连接')
|
|
128
|
+
ws.close()
|
|
129
|
+
}, 5000)
|
|
130
|
+
|
|
131
|
+
ws.on('message', (data) => {
|
|
132
|
+
try {
|
|
133
|
+
const message = JSON.parse(data.toString()) as BaseMessage
|
|
134
|
+
|
|
135
|
+
// 处理注册消息
|
|
136
|
+
if (message.type === MessageType.REGISTER) {
|
|
137
|
+
clearTimeout(registerTimeout)
|
|
138
|
+
this.handleRegister(ws, message as RegisterMessage)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 其他消息需要已注册
|
|
143
|
+
const managedWs = ws as ManagedWebSocket
|
|
144
|
+
if (!managedWs.connectionId) {
|
|
145
|
+
this.logger.warn('未注册的连接发送消息,忽略')
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.handleMessage(managedWs, message)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.logger.error('消息解析失败:', error)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
ws.on('close', () => {
|
|
156
|
+
clearTimeout(registerTimeout)
|
|
157
|
+
const managedWs = ws as ManagedWebSocket
|
|
158
|
+
if (managedWs.connectionId) {
|
|
159
|
+
this.requestHandler.cleanupConnectionRequests(managedWs.connectionId)
|
|
160
|
+
this.connectionManager.removeConnection(managedWs)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
ws.on('error', (error) => {
|
|
165
|
+
this.logger.error('WebSocket 错误:', error)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 处理注册消息
|
|
171
|
+
*/
|
|
172
|
+
private handleRegister(ws: WebSocket, message: RegisterMessage): void {
|
|
173
|
+
const { connectionType, pageUrl, pageId } = message.data
|
|
174
|
+
|
|
175
|
+
const managedWs = this.connectionManager.addConnection(
|
|
176
|
+
ws,
|
|
177
|
+
connectionType,
|
|
178
|
+
pageUrl,
|
|
179
|
+
pageId
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
// 发送注册确认
|
|
183
|
+
const ack: ResponseMessage = {
|
|
184
|
+
id: message.id || '',
|
|
185
|
+
type: MessageType.REGISTER_ACK,
|
|
186
|
+
success: true,
|
|
187
|
+
data: {
|
|
188
|
+
connectionId: managedWs.connectionId,
|
|
189
|
+
pageUrl,
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ws.send(JSON.stringify(ack))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 处理消息
|
|
198
|
+
*/
|
|
199
|
+
private handleMessage(ws: ManagedWebSocket, message: BaseMessage): void {
|
|
200
|
+
this.connectionManager.updateLastActive(ws)
|
|
201
|
+
|
|
202
|
+
switch (message.type) {
|
|
203
|
+
case MessageType.PING:
|
|
204
|
+
this.handlePing(ws, message)
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
case MessageType.RESPONSE:
|
|
208
|
+
case MessageType.ERROR:
|
|
209
|
+
// Provider 返回的响应
|
|
210
|
+
this.requestHandler.handleResponse(message as ResponseMessage)
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
// Consumer 发送的请求
|
|
215
|
+
if (ws.connectionInfo.type === ConnectionType.CONSUMER) {
|
|
216
|
+
this.requestHandler.handleRequest(ws, message as any)
|
|
217
|
+
}
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 处理心跳
|
|
224
|
+
*/
|
|
225
|
+
private handlePing(ws: ManagedWebSocket, message: BaseMessage): void {
|
|
226
|
+
const pong = {
|
|
227
|
+
type: MessageType.PONG,
|
|
228
|
+
timestamp: (message as any).timestamp || Date.now(),
|
|
229
|
+
}
|
|
230
|
+
ws.send(JSON.stringify(pong))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 停止服务器
|
|
235
|
+
*/
|
|
236
|
+
async stop(): Promise<void> {
|
|
237
|
+
if (!this.isRunning || !this.wss) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.logger.info('正在停止 Server...')
|
|
242
|
+
|
|
243
|
+
// 清理所有请求
|
|
244
|
+
this.requestHandler.cleanupAll()
|
|
245
|
+
|
|
246
|
+
// 关闭所有连接
|
|
247
|
+
this.connectionManager.closeAll()
|
|
248
|
+
|
|
249
|
+
// 关闭服务器
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
this.wss!.close(() => {
|
|
252
|
+
this.isRunning = false
|
|
253
|
+
this.wss = null
|
|
254
|
+
this.logger.info('Server 已停止')
|
|
255
|
+
resolve()
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 获取运行状态
|
|
262
|
+
*/
|
|
263
|
+
getStatus(): {
|
|
264
|
+
running: boolean
|
|
265
|
+
port: number
|
|
266
|
+
stats: { providers: number; consumers: number; total: number }
|
|
267
|
+
connectedPages: string[]
|
|
268
|
+
} {
|
|
269
|
+
return {
|
|
270
|
+
running: this.isRunning,
|
|
271
|
+
port: this.port,
|
|
272
|
+
stats: this.connectionManager.getStats(),
|
|
273
|
+
connectedPages: this.connectionManager.getConnectedPageUrls(),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 获取端口
|
|
279
|
+
*/
|
|
280
|
+
getPort(): number {
|
|
281
|
+
return this.port
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 是否运行中
|
|
286
|
+
*/
|
|
287
|
+
isServerRunning(): boolean {
|
|
288
|
+
return this.isRunning
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 创建服务器实例
|
|
294
|
+
*/
|
|
295
|
+
export function createServer(options?: ServerOptions): MGServer {
|
|
296
|
+
return new MGServer(options)
|
|
297
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MG Plugin 常量定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
// ==================== 端口配置 ====================
|
|
9
|
+
|
|
10
|
+
/** 默认端口 */
|
|
11
|
+
export const DEFAULT_PORT = 9527
|
|
12
|
+
|
|
13
|
+
/** 端口范围:起始 */
|
|
14
|
+
export const PORT_RANGE_START = 9527
|
|
15
|
+
|
|
16
|
+
/** 端口范围:结束 */
|
|
17
|
+
export const PORT_RANGE_END = 9536
|
|
18
|
+
|
|
19
|
+
/** 最大尝试端口数 */
|
|
20
|
+
export const MAX_PORT_ATTEMPTS = 10
|
|
21
|
+
|
|
22
|
+
/** 端口扫描超时(毫秒) */
|
|
23
|
+
export const PORT_SCAN_TIMEOUT = 500
|
|
24
|
+
|
|
25
|
+
// ==================== 路径配置 ====================
|
|
26
|
+
|
|
27
|
+
/** 配置目录 */
|
|
28
|
+
export const CONFIG_DIR = join(homedir(), '.mg-plugin')
|
|
29
|
+
|
|
30
|
+
/** Server 状态文件 */
|
|
31
|
+
export const SERVER_INFO_FILE = join(CONFIG_DIR, 'server.json')
|
|
32
|
+
|
|
33
|
+
/** 日志目录 */
|
|
34
|
+
export const LOG_DIR = join(CONFIG_DIR, 'logs')
|
|
35
|
+
|
|
36
|
+
/** Server 日志文件 */
|
|
37
|
+
export const SERVER_LOG_FILE = join(LOG_DIR, 'server.log')
|
|
38
|
+
|
|
39
|
+
// ==================== 超时配置 ====================
|
|
40
|
+
|
|
41
|
+
/** 心跳间隔(毫秒)- 30 秒 */
|
|
42
|
+
export const HEARTBEAT_INTERVAL = 30000
|
|
43
|
+
|
|
44
|
+
/** 心跳超时(毫秒)- 90 秒(3 次心跳) */
|
|
45
|
+
export const HEARTBEAT_TIMEOUT = 90000
|
|
46
|
+
|
|
47
|
+
/** 请求超时(毫秒)- 30 秒 */
|
|
48
|
+
export const REQUEST_TIMEOUT = 30000
|
|
49
|
+
|
|
50
|
+
/** Server 启动等待超时(毫秒)- 5 秒 */
|
|
51
|
+
export const SERVER_START_TIMEOUT = 5000
|
|
52
|
+
|
|
53
|
+
/** CLI 重试间隔(毫秒) */
|
|
54
|
+
export const RETRY_INTERVALS = [1000, 2000, 4000]
|
|
55
|
+
|
|
56
|
+
/** 最大重试次数 */
|
|
57
|
+
export const MAX_RETRY_COUNT = 3
|
|
58
|
+
|
|
59
|
+
// ==================== 连接类型 ====================
|
|
60
|
+
|
|
61
|
+
/** 连接类型 */
|
|
62
|
+
export enum ConnectionType {
|
|
63
|
+
/** 获取端 (CLI/MCP) */
|
|
64
|
+
CONSUMER = 'consumer',
|
|
65
|
+
/** 提供端 (Injector) */
|
|
66
|
+
PROVIDER = 'provider',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ==================== 消息类型 ====================
|
|
70
|
+
|
|
71
|
+
/** WebSocket 消息类型 */
|
|
72
|
+
export enum MessageType {
|
|
73
|
+
// 系统消息
|
|
74
|
+
PING = 'ping',
|
|
75
|
+
PONG = 'pong',
|
|
76
|
+
REGISTER = 'register',
|
|
77
|
+
REGISTER_ACK = 'register_ack',
|
|
78
|
+
|
|
79
|
+
// 业务消息
|
|
80
|
+
GET_NODE_BY_ID = 'get_node_by_id',
|
|
81
|
+
GET_ALL_NODES = 'get_all_nodes',
|
|
82
|
+
GET_SELECTION = 'get_selection',
|
|
83
|
+
EXPORT_IMAGE = 'export_image',
|
|
84
|
+
EXECUTE_CODE = 'execute_code',
|
|
85
|
+
GET_ALL_PAGES = 'get_all_pages',
|
|
86
|
+
|
|
87
|
+
// 响应
|
|
88
|
+
RESPONSE = 'response',
|
|
89
|
+
ERROR = 'error',
|
|
90
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MG Plugin 错误码定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** 错误码枚举 */
|
|
6
|
+
export enum ErrorCode {
|
|
7
|
+
/** 无法连接到 MG Server */
|
|
8
|
+
CONNECTION_FAILED = 'E001',
|
|
9
|
+
/** 连接超时 */
|
|
10
|
+
CONNECTION_TIMEOUT = 'E002',
|
|
11
|
+
/** 没有 MasterGo 页面连接到 Server */
|
|
12
|
+
NO_PAGE_CONNECTED = 'E003',
|
|
13
|
+
/** 未找到匹配的页面 */
|
|
14
|
+
PAGE_NOT_FOUND = 'E004',
|
|
15
|
+
/** 节点不存在 */
|
|
16
|
+
NODE_NOT_FOUND = 'E005',
|
|
17
|
+
/** 没有选中任何节点 */
|
|
18
|
+
NO_SELECTION = 'E006',
|
|
19
|
+
/** mg 对象不可用 */
|
|
20
|
+
MG_UNAVAILABLE = 'E007',
|
|
21
|
+
/** 导出图片失败 */
|
|
22
|
+
EXPORT_FAILED = 'E008',
|
|
23
|
+
/** 文件写入失败 */
|
|
24
|
+
FILE_WRITE_FAILED = 'E009',
|
|
25
|
+
/** 无效的 mgp:// 链接格式 */
|
|
26
|
+
INVALID_LINK = 'E010',
|
|
27
|
+
/** 参数校验失败 */
|
|
28
|
+
INVALID_PARAMS = 'E011',
|
|
29
|
+
/** 请求超时 */
|
|
30
|
+
REQUEST_TIMEOUT = 'E012',
|
|
31
|
+
/** 所有备选端口均被占用 */
|
|
32
|
+
PORT_EXHAUSTED = 'E013',
|
|
33
|
+
/** 无法发现 Server (端口扫描失败) */
|
|
34
|
+
SERVER_DISCOVERY_FAILED = 'E014',
|
|
35
|
+
/** 自动启动 Server 失败 */
|
|
36
|
+
SERVER_START_FAILED = 'E015',
|
|
37
|
+
/** Server 已在运行中 */
|
|
38
|
+
SERVER_ALREADY_RUNNING = 'E016',
|
|
39
|
+
/** 连接断开 */
|
|
40
|
+
CONNECTION_LOST = 'E017',
|
|
41
|
+
/** 未知错误 */
|
|
42
|
+
UNKNOWN_ERROR = 'E099',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 错误码对应的错误名称 */
|
|
46
|
+
export const ErrorNames: Record<ErrorCode, string> = {
|
|
47
|
+
[ErrorCode.CONNECTION_FAILED]: 'CONNECTION_FAILED',
|
|
48
|
+
[ErrorCode.CONNECTION_TIMEOUT]: 'CONNECTION_TIMEOUT',
|
|
49
|
+
[ErrorCode.NO_PAGE_CONNECTED]: 'NO_PAGE_CONNECTED',
|
|
50
|
+
[ErrorCode.PAGE_NOT_FOUND]: 'PAGE_NOT_FOUND',
|
|
51
|
+
[ErrorCode.NODE_NOT_FOUND]: 'NODE_NOT_FOUND',
|
|
52
|
+
[ErrorCode.NO_SELECTION]: 'NO_SELECTION',
|
|
53
|
+
[ErrorCode.MG_UNAVAILABLE]: 'MG_UNAVAILABLE',
|
|
54
|
+
[ErrorCode.EXPORT_FAILED]: 'EXPORT_FAILED',
|
|
55
|
+
[ErrorCode.FILE_WRITE_FAILED]: 'FILE_WRITE_FAILED',
|
|
56
|
+
[ErrorCode.INVALID_LINK]: 'INVALID_LINK',
|
|
57
|
+
[ErrorCode.INVALID_PARAMS]: 'INVALID_PARAMS',
|
|
58
|
+
[ErrorCode.REQUEST_TIMEOUT]: 'REQUEST_TIMEOUT',
|
|
59
|
+
[ErrorCode.PORT_EXHAUSTED]: 'PORT_EXHAUSTED',
|
|
60
|
+
[ErrorCode.SERVER_DISCOVERY_FAILED]: 'SERVER_DISCOVERY_FAILED',
|
|
61
|
+
[ErrorCode.SERVER_START_FAILED]: 'SERVER_START_FAILED',
|
|
62
|
+
[ErrorCode.SERVER_ALREADY_RUNNING]: 'SERVER_ALREADY_RUNNING',
|
|
63
|
+
[ErrorCode.CONNECTION_LOST]: 'CONNECTION_LOST',
|
|
64
|
+
[ErrorCode.UNKNOWN_ERROR]: 'UNKNOWN_ERROR',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 错误码对应的默认消息 */
|
|
68
|
+
export const ErrorMessages: Record<ErrorCode, string> = {
|
|
69
|
+
[ErrorCode.CONNECTION_FAILED]: '无法连接到 MG Server',
|
|
70
|
+
[ErrorCode.CONNECTION_TIMEOUT]: '连接超时',
|
|
71
|
+
[ErrorCode.NO_PAGE_CONNECTED]: '没有 MasterGo 页面连接到 Server',
|
|
72
|
+
[ErrorCode.PAGE_NOT_FOUND]: '未找到匹配的页面',
|
|
73
|
+
[ErrorCode.NODE_NOT_FOUND]: '节点不存在',
|
|
74
|
+
[ErrorCode.NO_SELECTION]: '没有选中任何节点',
|
|
75
|
+
[ErrorCode.MG_UNAVAILABLE]: 'mg 对象不可用',
|
|
76
|
+
[ErrorCode.EXPORT_FAILED]: '导出图片失败',
|
|
77
|
+
[ErrorCode.FILE_WRITE_FAILED]: '文件写入失败',
|
|
78
|
+
[ErrorCode.INVALID_LINK]: '无效的 mgp:// 链接格式',
|
|
79
|
+
[ErrorCode.INVALID_PARAMS]: '参数校验失败',
|
|
80
|
+
[ErrorCode.REQUEST_TIMEOUT]: '请求超时',
|
|
81
|
+
[ErrorCode.PORT_EXHAUSTED]: '所有备选端口均被占用',
|
|
82
|
+
[ErrorCode.SERVER_DISCOVERY_FAILED]: '无法发现 Server (端口扫描失败)',
|
|
83
|
+
[ErrorCode.SERVER_START_FAILED]: '自动启动 Server 失败',
|
|
84
|
+
[ErrorCode.SERVER_ALREADY_RUNNING]: 'Server 已在运行中',
|
|
85
|
+
[ErrorCode.CONNECTION_LOST]: '连接断开',
|
|
86
|
+
[ErrorCode.UNKNOWN_ERROR]: '未知错误',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** MG Plugin 错误类 */
|
|
90
|
+
export class MGError extends Error {
|
|
91
|
+
/** 错误码 */
|
|
92
|
+
code: ErrorCode
|
|
93
|
+
/** 错误名称 */
|
|
94
|
+
errorName: string
|
|
95
|
+
/** 额外详情 */
|
|
96
|
+
details?: Record<string, unknown>
|
|
97
|
+
|
|
98
|
+
constructor(code: ErrorCode, message?: string, details?: Record<string, unknown>) {
|
|
99
|
+
super(message || ErrorMessages[code])
|
|
100
|
+
this.name = 'MGError'
|
|
101
|
+
this.code = code
|
|
102
|
+
this.errorName = ErrorNames[code]
|
|
103
|
+
this.details = details
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 转换为 JSON 格式 */
|
|
107
|
+
toJSON() {
|
|
108
|
+
return {
|
|
109
|
+
code: this.code,
|
|
110
|
+
name: this.errorName,
|
|
111
|
+
message: this.message,
|
|
112
|
+
details: this.details,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 格式化输出 */
|
|
117
|
+
toString() {
|
|
118
|
+
return `错误 [${this.code}]: ${this.message}`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 创建错误
|
|
124
|
+
*/
|
|
125
|
+
export function createError(
|
|
126
|
+
code: ErrorCode,
|
|
127
|
+
message?: string,
|
|
128
|
+
details?: Record<string, unknown>
|
|
129
|
+
): MGError {
|
|
130
|
+
return new MGError(code, message, details)
|
|
131
|
+
}
|