dg-lab-mcp-sse-server 1.0.3
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/README.md +179 -0
- package/dist/app.d.ts +43 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3165 -0
- package/dist/cli.js.map +7 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +132 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3163 -0
- package/dist/index.js.map +7 -0
- package/dist/jsonrpc-handler.d.ts +79 -0
- package/dist/jsonrpc-handler.d.ts.map +1 -0
- package/dist/mcp-protocol.d.ts +50 -0
- package/dist/mcp-protocol.d.ts.map +1 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/session-manager.d.ts +209 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/sse-transport.d.ts +88 -0
- package/dist/sse-transport.d.ts.map +1 -0
- package/dist/tool-manager.d.ts +159 -0
- package/dist/tool-manager.d.ts.map +1 -0
- package/dist/tools/control-tools.d.ts +126 -0
- package/dist/tools/control-tools.d.ts.map +1 -0
- package/dist/tools/device-tools.d.ts +33 -0
- package/dist/tools/device-tools.d.ts.map +1 -0
- package/dist/tools/waveform-tools.d.ts +53 -0
- package/dist/tools/waveform-tools.d.ts.map +1 -0
- package/dist/types/jsonrpc.d.ts +157 -0
- package/dist/types/jsonrpc.d.ts.map +1 -0
- package/dist/waveform-parser.d.ts +259 -0
- package/dist/waveform-parser.d.ts.map +1 -0
- package/dist/waveform-storage.d.ts +101 -0
- package/dist/waveform-storage.d.ts.map +1 -0
- package/dist/ws-bridge.d.ts +107 -0
- package/dist/ws-bridge.d.ts.map +1 -0
- package/dist/ws-server.d.ts +174 -0
- package/dist/ws-server.d.ts.map +1 -0
- package/package.json +63 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var AppError = class extends Error {
|
|
8
|
+
/** 错误码,用于程序化处理 */
|
|
9
|
+
code;
|
|
10
|
+
/** 是否可恢复(true 表示可以继续运行,false 表示需要终止) */
|
|
11
|
+
recoverable;
|
|
12
|
+
/** 错误上下文信息,包含相关的调试数据 */
|
|
13
|
+
context;
|
|
14
|
+
constructor(code, message, options) {
|
|
15
|
+
super(message, { cause: options?.cause });
|
|
16
|
+
this.name = "AppError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.recoverable = options?.recoverable ?? true;
|
|
19
|
+
this.context = options?.context;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 格式化错误信息,方便日志输出
|
|
23
|
+
*
|
|
24
|
+
* 输出格式包含错误码、消息、上下文和原因,一目了然。
|
|
25
|
+
*/
|
|
26
|
+
toLogString() {
|
|
27
|
+
const parts = [`[${this.code}] ${this.message}`];
|
|
28
|
+
if (this.context) {
|
|
29
|
+
parts.push(`Context: ${JSON.stringify(this.context)}`);
|
|
30
|
+
}
|
|
31
|
+
if (this.cause) {
|
|
32
|
+
parts.push(`Cause: ${this.cause}`);
|
|
33
|
+
}
|
|
34
|
+
return parts.join("\n ");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ConfigError = class extends AppError {
|
|
38
|
+
constructor(message, options) {
|
|
39
|
+
super(options?.code ?? "CONFIG_LOAD_FAILED" /* CONFIG_LOAD_FAILED */, message, {
|
|
40
|
+
recoverable: false,
|
|
41
|
+
context: options?.context,
|
|
42
|
+
cause: options?.cause
|
|
43
|
+
});
|
|
44
|
+
this.name = "ConfigError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var ConnectionError = class extends AppError {
|
|
48
|
+
constructor(message, options) {
|
|
49
|
+
super(options?.code ?? "CONN_DEVICE_NOT_FOUND" /* CONN_DEVICE_NOT_FOUND */, message, {
|
|
50
|
+
recoverable: true,
|
|
51
|
+
context: options?.context,
|
|
52
|
+
cause: options?.cause
|
|
53
|
+
});
|
|
54
|
+
this.name = "ConnectionError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var WaveformError = class extends AppError {
|
|
58
|
+
constructor(message, options) {
|
|
59
|
+
super(options?.code ?? "WAVEFORM_PARSE_FAILED" /* WAVEFORM_PARSE_FAILED */, message, {
|
|
60
|
+
recoverable: true,
|
|
61
|
+
context: options?.context,
|
|
62
|
+
cause: options?.cause
|
|
63
|
+
});
|
|
64
|
+
this.name = "WaveformError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/config.ts
|
|
69
|
+
function getEnvString(key, defaultValue) {
|
|
70
|
+
return process.env[key] ?? defaultValue;
|
|
71
|
+
}
|
|
72
|
+
function getEnvNumber(key, defaultValue) {
|
|
73
|
+
const value = process.env[key];
|
|
74
|
+
if (value === void 0) return defaultValue;
|
|
75
|
+
const parsed = parseInt(value, 10);
|
|
76
|
+
if (isNaN(parsed)) {
|
|
77
|
+
throw new ConfigError(`\u73AF\u5883\u53D8\u91CF ${key} \u7684\u503C\u65E0\u6548: ${value}`, {
|
|
78
|
+
code: "CONFIG_LOAD_FAILED" /* CONFIG_LOAD_FAILED */,
|
|
79
|
+
context: { key, value }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
function loadConfig() {
|
|
85
|
+
const config = {
|
|
86
|
+
port: getEnvNumber("PORT", 3323),
|
|
87
|
+
publicIp: getEnvString("PUBLIC_IP", ""),
|
|
88
|
+
ssePath: getEnvString("SSE_PATH", "/sse"),
|
|
89
|
+
postPath: getEnvString("POST_PATH", "/message"),
|
|
90
|
+
sessionStorePath: getEnvString("SESSION_STORE_PATH", "./data/sessions.json"),
|
|
91
|
+
waveformStorePath: getEnvString("WAVEFORM_STORE_PATH", "./data/waveforms.json"),
|
|
92
|
+
heartbeatInterval: getEnvNumber("HEARTBEAT_INTERVAL", 3e4),
|
|
93
|
+
staleDeviceTimeout: getEnvNumber("STALE_DEVICE_TIMEOUT", 36e5),
|
|
94
|
+
connectionTimeoutMinutes: getEnvNumber("CONNECTION_TIMEOUT_MINUTES", 5)
|
|
95
|
+
};
|
|
96
|
+
validateConfig(config);
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
function validateConfig(config) {
|
|
100
|
+
if (config.port < 1 || config.port > 65535) {
|
|
101
|
+
throw new ConfigError(`\u7AEF\u53E3\u65E0\u6548: ${config.port}\uFF0C\u5FC5\u987B\u5728 1-65535 \u8303\u56F4\u5185`, {
|
|
102
|
+
code: "CONFIG_INVALID_PORT" /* CONFIG_INVALID_PORT */,
|
|
103
|
+
context: { port: config.port }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (config.publicIp) {
|
|
107
|
+
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
108
|
+
if (!ipv4Regex.test(config.publicIp)) {
|
|
109
|
+
console.warn(`[\u914D\u7F6E] \u26A0\uFE0F \u516C\u7F51IP\u683C\u5F0F\u65E0\u6548: ${config.publicIp}\uFF0C\u5C06\u4F7F\u7528\u672C\u5730IP`);
|
|
110
|
+
config.publicIp = "";
|
|
111
|
+
} else {
|
|
112
|
+
const parts = config.publicIp.split(".");
|
|
113
|
+
if (parts.some((part) => parseInt(part, 10) > 255)) {
|
|
114
|
+
console.warn(`[\u914D\u7F6E] \u26A0\uFE0F \u516C\u7F51IP\u683C\u5F0F\u65E0\u6548: ${config.publicIp}\uFF0C\u6BCF\u6BB5\u5FC5\u987B\u57280-255\u8303\u56F4\u5185\uFF0C\u5C06\u4F7F\u7528\u672C\u5730IP`);
|
|
115
|
+
config.publicIp = "";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!config.ssePath.startsWith("/")) {
|
|
120
|
+
throw new ConfigError(`SSE \u8DEF\u5F84\u65E0\u6548: ${config.ssePath}\uFF0C\u5FC5\u987B\u4EE5 / \u5F00\u5934`, {
|
|
121
|
+
code: "CONFIG_INVALID_PATH" /* CONFIG_INVALID_PATH */,
|
|
122
|
+
context: { path: config.ssePath, type: "ssePath" }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!config.postPath.startsWith("/")) {
|
|
126
|
+
throw new ConfigError(`POST \u8DEF\u5F84\u65E0\u6548: ${config.postPath}\uFF0C\u5FC5\u987B\u4EE5 / \u5F00\u5934`, {
|
|
127
|
+
code: "CONFIG_INVALID_PATH" /* CONFIG_INVALID_PATH */,
|
|
128
|
+
context: { path: config.postPath, type: "postPath" }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (config.heartbeatInterval < 1e3) {
|
|
132
|
+
throw new ConfigError(`\u5FC3\u8DF3\u95F4\u9694\u65E0\u6548: ${config.heartbeatInterval}\uFF0C\u5FC5\u987B\u81F3\u5C11 1000ms`, {
|
|
133
|
+
code: "CONFIG_LOAD_FAILED" /* CONFIG_LOAD_FAILED */,
|
|
134
|
+
context: { heartbeatInterval: config.heartbeatInterval }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (config.staleDeviceTimeout < 6e4) {
|
|
138
|
+
throw new ConfigError(`\u8BBE\u5907\u8FC7\u671F\u8D85\u65F6\u65E0\u6548: ${config.staleDeviceTimeout}\uFF0C\u5FC5\u987B\u81F3\u5C11 60000ms`, {
|
|
139
|
+
code: "CONFIG_LOAD_FAILED" /* CONFIG_LOAD_FAILED */,
|
|
140
|
+
context: { staleDeviceTimeout: config.staleDeviceTimeout }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (config.connectionTimeoutMinutes < 1 || config.connectionTimeoutMinutes > 60) {
|
|
144
|
+
throw new ConfigError(`\u8FDE\u63A5\u8D85\u65F6\u65F6\u95F4\u65E0\u6548: ${config.connectionTimeoutMinutes}\uFF0C\u5FC5\u987B\u5728 1-60 \u5206\u949F\u8303\u56F4\u5185`, {
|
|
145
|
+
code: "CONFIG_LOAD_FAILED" /* CONFIG_LOAD_FAILED */,
|
|
146
|
+
context: { connectionTimeoutMinutes: config.connectionTimeoutMinutes }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
var configInstance = null;
|
|
151
|
+
function getConfig() {
|
|
152
|
+
if (!configInstance) {
|
|
153
|
+
configInstance = loadConfig();
|
|
154
|
+
}
|
|
155
|
+
return configInstance;
|
|
156
|
+
}
|
|
157
|
+
function getLocalIP() {
|
|
158
|
+
const interfaces = os.networkInterfaces();
|
|
159
|
+
for (const name of Object.keys(interfaces)) {
|
|
160
|
+
for (const iface of interfaces[name] || []) {
|
|
161
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
162
|
+
return iface.address;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return "localhost";
|
|
167
|
+
}
|
|
168
|
+
function getEffectiveIP(config) {
|
|
169
|
+
const cfg = config || getConfig();
|
|
170
|
+
return cfg.publicIp || getLocalIP();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/server.ts
|
|
174
|
+
import express from "express";
|
|
175
|
+
|
|
176
|
+
// src/sse-transport.ts
|
|
177
|
+
import { v4 as uuidv4 } from "uuid";
|
|
178
|
+
|
|
179
|
+
// src/types/jsonrpc.ts
|
|
180
|
+
var JSON_RPC_ERRORS = {
|
|
181
|
+
/** 解析错误 - 无效的 JSON */
|
|
182
|
+
PARSE_ERROR: -32700,
|
|
183
|
+
/** 无效请求 - JSON 不是有效的请求对象 */
|
|
184
|
+
INVALID_REQUEST: -32600,
|
|
185
|
+
/** 方法未找到 */
|
|
186
|
+
METHOD_NOT_FOUND: -32601,
|
|
187
|
+
/** 无效参数 */
|
|
188
|
+
INVALID_PARAMS: -32602,
|
|
189
|
+
/** 内部错误 */
|
|
190
|
+
INTERNAL_ERROR: -32603
|
|
191
|
+
};
|
|
192
|
+
function isJsonRpcRequest(msg) {
|
|
193
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
194
|
+
const obj = msg;
|
|
195
|
+
return obj.jsonrpc === "2.0" && "method" in obj && typeof obj.method === "string" && "id" in obj && (typeof obj.id === "string" || typeof obj.id === "number");
|
|
196
|
+
}
|
|
197
|
+
function isJsonRpcNotification(msg) {
|
|
198
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
199
|
+
const obj = msg;
|
|
200
|
+
return obj.jsonrpc === "2.0" && "method" in obj && typeof obj.method === "string" && !("id" in obj);
|
|
201
|
+
}
|
|
202
|
+
function serialize(message) {
|
|
203
|
+
return JSON.stringify(message);
|
|
204
|
+
}
|
|
205
|
+
function deserialize(data) {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(data);
|
|
208
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: {
|
|
212
|
+
code: JSON_RPC_ERRORS.INVALID_REQUEST,
|
|
213
|
+
message: "\u65E0\u6548\u8BF7\u6C42: \u4E0D\u662F\u5BF9\u8C61"
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (parsed.jsonrpc !== "2.0") {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: {
|
|
221
|
+
code: JSON_RPC_ERRORS.INVALID_REQUEST,
|
|
222
|
+
message: "\u65E0\u6548\u8BF7\u6C42: \u7F3A\u5C11\u6216\u65E0\u6548\u7684 jsonrpc \u7248\u672C"
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { success: true, message: parsed };
|
|
227
|
+
} catch {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: {
|
|
231
|
+
code: JSON_RPC_ERRORS.PARSE_ERROR,
|
|
232
|
+
message: "\u89E3\u6790\u9519\u8BEF: \u65E0\u6548\u7684 JSON"
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function createSuccessResponse(id, result) {
|
|
238
|
+
return { jsonrpc: "2.0", id, result };
|
|
239
|
+
}
|
|
240
|
+
function createErrorResponse(id, code, message, data) {
|
|
241
|
+
const error = { code, message };
|
|
242
|
+
if (data !== void 0) error.data = data;
|
|
243
|
+
return { jsonrpc: "2.0", id, error };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/sse-transport.ts
|
|
247
|
+
var SSETransport = class {
|
|
248
|
+
connections = /* @__PURE__ */ new Map();
|
|
249
|
+
postPath;
|
|
250
|
+
baseUrl;
|
|
251
|
+
/**
|
|
252
|
+
* 创建 SSE 传输层
|
|
253
|
+
* @param postPath - POST 端点路径
|
|
254
|
+
* @param baseUrl - 基础 URL(可选)
|
|
255
|
+
*/
|
|
256
|
+
constructor(postPath, baseUrl = "") {
|
|
257
|
+
this.postPath = postPath;
|
|
258
|
+
this.baseUrl = baseUrl;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 建立 SSE 连接并发送 endpoint 事件
|
|
262
|
+
* @param req - HTTP 请求
|
|
263
|
+
* @param res - HTTP 响应
|
|
264
|
+
* @returns SSE 连接信息
|
|
265
|
+
*/
|
|
266
|
+
connect(req, res) {
|
|
267
|
+
const connectionId = uuidv4();
|
|
268
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
269
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
270
|
+
res.setHeader("Connection", "keep-alive");
|
|
271
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
272
|
+
res.flushHeaders();
|
|
273
|
+
const postEndpoint = `${this.baseUrl}${this.postPath}?sessionId=${connectionId}`;
|
|
274
|
+
const connection = {
|
|
275
|
+
id: connectionId,
|
|
276
|
+
response: res,
|
|
277
|
+
postEndpoint,
|
|
278
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
279
|
+
};
|
|
280
|
+
this.connections.set(connectionId, connection);
|
|
281
|
+
this.sendEvent(connectionId, "endpoint", postEndpoint);
|
|
282
|
+
req.on("close", () => {
|
|
283
|
+
this.disconnect(connectionId);
|
|
284
|
+
});
|
|
285
|
+
return connection;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 向指定连接发送 SSE 事件
|
|
289
|
+
* @param connectionId - 连接 ID
|
|
290
|
+
* @param event - 事件名
|
|
291
|
+
* @param data - 数据
|
|
292
|
+
* @returns 是否发送成功
|
|
293
|
+
*/
|
|
294
|
+
sendEvent(connectionId, event, data) {
|
|
295
|
+
const connection = this.connections.get(connectionId);
|
|
296
|
+
if (!connection) return false;
|
|
297
|
+
try {
|
|
298
|
+
connection.response.write(`event: ${event}
|
|
299
|
+
`);
|
|
300
|
+
connection.response.write(`data: ${data}
|
|
301
|
+
|
|
302
|
+
`);
|
|
303
|
+
return true;
|
|
304
|
+
} catch {
|
|
305
|
+
this.disconnect(connectionId);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* 通过 SSE 发送 JSON-RPC 消息
|
|
311
|
+
* @param connectionId - 连接 ID
|
|
312
|
+
* @param message - JSON-RPC 消息
|
|
313
|
+
* @returns 是否发送成功
|
|
314
|
+
*/
|
|
315
|
+
send(connectionId, message) {
|
|
316
|
+
const data = serialize(message);
|
|
317
|
+
return this.sendEvent(connectionId, "message", data);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 断开并清理 SSE 连接
|
|
321
|
+
* @param connectionId - 连接 ID
|
|
322
|
+
*/
|
|
323
|
+
disconnect(connectionId) {
|
|
324
|
+
const connection = this.connections.get(connectionId);
|
|
325
|
+
if (connection) {
|
|
326
|
+
try {
|
|
327
|
+
connection.response.end();
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
this.connections.delete(connectionId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 根据 ID 获取连接
|
|
335
|
+
* @param connectionId - 连接 ID
|
|
336
|
+
* @returns 连接信息或 undefined
|
|
337
|
+
*/
|
|
338
|
+
getConnection(connectionId) {
|
|
339
|
+
return this.connections.get(connectionId);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* 检查连接是否存在
|
|
343
|
+
* @param connectionId - 连接 ID
|
|
344
|
+
* @returns 是否存在
|
|
345
|
+
*/
|
|
346
|
+
hasConnection(connectionId) {
|
|
347
|
+
return this.connections.has(connectionId);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* 获取所有活跃连接 ID
|
|
351
|
+
* @returns 连接 ID 数组
|
|
352
|
+
*/
|
|
353
|
+
getConnectionIds() {
|
|
354
|
+
return Array.from(this.connections.keys());
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* 向所有连接广播消息
|
|
358
|
+
* @param message - JSON-RPC 消息
|
|
359
|
+
*/
|
|
360
|
+
broadcast(message) {
|
|
361
|
+
for (const connectionId of this.connections.keys()) {
|
|
362
|
+
this.send(connectionId, message);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 获取连接数量
|
|
367
|
+
*/
|
|
368
|
+
get connectionCount() {
|
|
369
|
+
return this.connections.size;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/jsonrpc-handler.ts
|
|
374
|
+
var JsonRpcHandler = class {
|
|
375
|
+
requestHandlers = /* @__PURE__ */ new Map();
|
|
376
|
+
notificationHandlers = /* @__PURE__ */ new Map();
|
|
377
|
+
options;
|
|
378
|
+
/**
|
|
379
|
+
* 创建 JSON-RPC 处理器
|
|
380
|
+
* @param options - 处理器选项
|
|
381
|
+
*/
|
|
382
|
+
constructor(options = {}) {
|
|
383
|
+
this.options = options;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 注册请求处理函数
|
|
387
|
+
* @param method - 方法名
|
|
388
|
+
* @param handler - 处理函数
|
|
389
|
+
*/
|
|
390
|
+
registerRequestHandler(method, handler) {
|
|
391
|
+
this.requestHandlers.set(method, handler);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 注册通知处理函数
|
|
395
|
+
* @param method - 方法名
|
|
396
|
+
* @param handler - 处理函数
|
|
397
|
+
*/
|
|
398
|
+
registerNotificationHandler(method, handler) {
|
|
399
|
+
this.notificationHandlers.set(method, handler);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 处理 JSON-RPC 消息
|
|
403
|
+
* @param data - JSON 字符串
|
|
404
|
+
* @returns 响应对象(通知返回 null)
|
|
405
|
+
*/
|
|
406
|
+
async handleMessage(data) {
|
|
407
|
+
const parseResult = deserialize(data);
|
|
408
|
+
if (!parseResult.success) {
|
|
409
|
+
const error2 = parseResult.error;
|
|
410
|
+
this.options.onError?.(error2);
|
|
411
|
+
return createErrorResponse(null, error2.code, error2.message, error2.data);
|
|
412
|
+
}
|
|
413
|
+
const message = parseResult.message;
|
|
414
|
+
if (isJsonRpcRequest(message)) {
|
|
415
|
+
return this.handleRequest(message);
|
|
416
|
+
}
|
|
417
|
+
if (isJsonRpcNotification(message)) {
|
|
418
|
+
await this.handleNotification(message);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const error = {
|
|
422
|
+
code: JSON_RPC_ERRORS.INVALID_REQUEST,
|
|
423
|
+
message: "\u65E0\u6548\u8BF7\u6C42: \u4E0D\u662F\u6709\u6548\u7684\u8BF7\u6C42\u6216\u901A\u77E5"
|
|
424
|
+
};
|
|
425
|
+
this.options.onError?.(error);
|
|
426
|
+
return createErrorResponse(null, error.code, error.message);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 处理请求
|
|
430
|
+
* @param request - 请求对象
|
|
431
|
+
* @returns 响应对象
|
|
432
|
+
*/
|
|
433
|
+
async handleRequest(request) {
|
|
434
|
+
this.options.onRequest?.(request.method, request.params);
|
|
435
|
+
const handler = this.requestHandlers.get(request.method);
|
|
436
|
+
if (!handler) {
|
|
437
|
+
const error = {
|
|
438
|
+
code: JSON_RPC_ERRORS.METHOD_NOT_FOUND,
|
|
439
|
+
message: `\u65B9\u6CD5\u672A\u627E\u5230: ${request.method}`
|
|
440
|
+
};
|
|
441
|
+
this.options.onError?.(error);
|
|
442
|
+
return createErrorResponse(request.id, error.code, error.message);
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const result = await handler(request.params);
|
|
446
|
+
return createSuccessResponse(request.id, result);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const error = {
|
|
449
|
+
code: JSON_RPC_ERRORS.INTERNAL_ERROR,
|
|
450
|
+
message: err instanceof Error ? err.message : "\u5185\u90E8\u9519\u8BEF"
|
|
451
|
+
};
|
|
452
|
+
this.options.onError?.(error);
|
|
453
|
+
return createErrorResponse(request.id, error.code, error.message);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 处理通知
|
|
458
|
+
* @param notification - 通知对象
|
|
459
|
+
*/
|
|
460
|
+
async handleNotification(notification) {
|
|
461
|
+
this.options.onNotification?.(notification.method, notification.params);
|
|
462
|
+
const handler = this.notificationHandlers.get(notification.method);
|
|
463
|
+
if (handler) {
|
|
464
|
+
try {
|
|
465
|
+
await handler(notification.params);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
const error = {
|
|
468
|
+
code: JSON_RPC_ERRORS.INTERNAL_ERROR,
|
|
469
|
+
message: err instanceof Error ? err.message : "\u5185\u90E8\u9519\u8BEF"
|
|
470
|
+
};
|
|
471
|
+
this.options.onError?.(error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 验证请求参数
|
|
477
|
+
* @param id - 请求 ID
|
|
478
|
+
* @param params - 参数
|
|
479
|
+
* @param required - 必需参数列表
|
|
480
|
+
* @returns 验证失败返回错误响应,成功返回 null
|
|
481
|
+
*/
|
|
482
|
+
validateParams(id, params, required) {
|
|
483
|
+
if (!params && required.length > 0) {
|
|
484
|
+
return createErrorResponse(
|
|
485
|
+
id,
|
|
486
|
+
JSON_RPC_ERRORS.INVALID_PARAMS,
|
|
487
|
+
`\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${required.join(", ")}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const missing = required.filter((key) => params?.[key] === void 0);
|
|
491
|
+
if (missing.length > 0) {
|
|
492
|
+
return createErrorResponse(
|
|
493
|
+
id,
|
|
494
|
+
JSON_RPC_ERRORS.INVALID_PARAMS,
|
|
495
|
+
`\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/server.ts
|
|
503
|
+
function createServer(config) {
|
|
504
|
+
const app = express();
|
|
505
|
+
app.use((req, res, next) => {
|
|
506
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
507
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
508
|
+
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
509
|
+
if (req.method === "OPTIONS") {
|
|
510
|
+
res.sendStatus(200);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
next();
|
|
514
|
+
});
|
|
515
|
+
app.use(express.json());
|
|
516
|
+
app.use(express.text({ type: "application/json" }));
|
|
517
|
+
const sseTransport = new SSETransport(config.postPath);
|
|
518
|
+
const jsonRpcHandler = new JsonRpcHandler({
|
|
519
|
+
onError: (error) => {
|
|
520
|
+
console.error("[JSON-RPC \u9519\u8BEF]", error);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
app.get(config.ssePath, (req, res) => {
|
|
524
|
+
console.log("[SSE] \u65B0\u8FDE\u63A5");
|
|
525
|
+
const connection = sseTransport.connect(req, res);
|
|
526
|
+
console.log(`[SSE] \u8FDE\u63A5\u5DF2\u5EFA\u7ACB: ${connection.id}`);
|
|
527
|
+
});
|
|
528
|
+
app.post(config.postPath, async (req, res) => {
|
|
529
|
+
const sessionId = req.query.sessionId;
|
|
530
|
+
if (!sessionId || !sseTransport.hasConnection(sessionId)) {
|
|
531
|
+
res.status(400).json({ error: "\u65E0\u6548\u6216\u7F3A\u5C11 sessionId" });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
let body;
|
|
535
|
+
if (typeof req.body === "string") {
|
|
536
|
+
body = req.body;
|
|
537
|
+
} else {
|
|
538
|
+
body = JSON.stringify(req.body);
|
|
539
|
+
}
|
|
540
|
+
console.log(`[POST] \u6536\u5230\u4F1A\u8BDD ${sessionId} \u7684\u6D88\u606F:`, body);
|
|
541
|
+
const response = await jsonRpcHandler.handleMessage(body);
|
|
542
|
+
if (response) {
|
|
543
|
+
sseTransport.send(sessionId, response);
|
|
544
|
+
}
|
|
545
|
+
res.status(202).json({ status: "accepted" });
|
|
546
|
+
});
|
|
547
|
+
app.get("/health", (_req, res) => {
|
|
548
|
+
res.json({
|
|
549
|
+
status: "ok",
|
|
550
|
+
connections: sseTransport.connectionCount
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
let httpServer = null;
|
|
554
|
+
return {
|
|
555
|
+
app,
|
|
556
|
+
httpServer: null,
|
|
557
|
+
sseTransport,
|
|
558
|
+
jsonRpcHandler,
|
|
559
|
+
async start() {
|
|
560
|
+
return new Promise((resolve) => {
|
|
561
|
+
httpServer = app.listen(config.port, () => {
|
|
562
|
+
console.log(`[\u670D\u52A1\u5668] MCP SSE \u670D\u52A1\u5668\u76D1\u542C\u7AEF\u53E3 ${config.port}`);
|
|
563
|
+
console.log(`[\u670D\u52A1\u5668] SSE \u7AEF\u70B9: ${config.ssePath}`);
|
|
564
|
+
console.log(`[\u670D\u52A1\u5668] POST \u7AEF\u70B9: ${config.postPath}`);
|
|
565
|
+
resolve();
|
|
566
|
+
});
|
|
567
|
+
this.httpServer = httpServer;
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
async stop() {
|
|
571
|
+
return new Promise((resolve) => {
|
|
572
|
+
if (httpServer && httpServer.listening) {
|
|
573
|
+
httpServer.close((err) => {
|
|
574
|
+
if (err) {
|
|
575
|
+
console.error("[\u670D\u52A1\u5668] \u5173\u95ED\u65F6\u51FA\u9519:", err);
|
|
576
|
+
} else {
|
|
577
|
+
console.log("[\u670D\u52A1\u5668] \u5DF2\u505C\u6B62");
|
|
578
|
+
}
|
|
579
|
+
resolve();
|
|
580
|
+
});
|
|
581
|
+
} else {
|
|
582
|
+
console.log("[\u670D\u52A1\u5668] \u672A\u542F\u52A8\u6216\u5DF2\u5173\u95ED");
|
|
583
|
+
resolve();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function broadcastNotification(server, method, params) {
|
|
590
|
+
const notification = {
|
|
591
|
+
jsonrpc: "2.0",
|
|
592
|
+
method,
|
|
593
|
+
params
|
|
594
|
+
};
|
|
595
|
+
server.sseTransport.broadcast(notification);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/mcp-protocol.ts
|
|
599
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
600
|
+
var SERVER_INFO = {
|
|
601
|
+
name: "dg-lab-mcp-server",
|
|
602
|
+
version: "1.0.0"
|
|
603
|
+
};
|
|
604
|
+
var SERVER_CAPABILITIES = {
|
|
605
|
+
tools: {
|
|
606
|
+
listChanged: true
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
function registerMCPProtocol(handler, onInitialized) {
|
|
610
|
+
handler.registerRequestHandler("initialize", async (params) => {
|
|
611
|
+
const initParams = params;
|
|
612
|
+
if (initParams?.protocolVersion && initParams.protocolVersion !== MCP_PROTOCOL_VERSION) {
|
|
613
|
+
console.log(`[MCP] \u5BA2\u6237\u7AEF\u8BF7\u6C42\u7684\u534F\u8BAE\u7248\u672C: ${initParams.protocolVersion}`);
|
|
614
|
+
}
|
|
615
|
+
const result = {
|
|
616
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
617
|
+
capabilities: SERVER_CAPABILITIES,
|
|
618
|
+
serverInfo: SERVER_INFO
|
|
619
|
+
};
|
|
620
|
+
console.log("[MCP] \u6536\u5230\u521D\u59CB\u5316\u8BF7\u6C42\uFF0C\u8FD4\u56DE\u670D\u52A1\u5668\u80FD\u529B");
|
|
621
|
+
return result;
|
|
622
|
+
});
|
|
623
|
+
handler.registerNotificationHandler("initialized", async () => {
|
|
624
|
+
console.log("[MCP] \u521D\u59CB\u5316\u5B8C\u6210");
|
|
625
|
+
onInitialized?.();
|
|
626
|
+
});
|
|
627
|
+
handler.registerRequestHandler("ping", async () => {
|
|
628
|
+
return {};
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/tool-manager.ts
|
|
633
|
+
function createToolResult(text) {
|
|
634
|
+
return {
|
|
635
|
+
content: [{ type: "text", text }]
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function createToolError(message) {
|
|
639
|
+
return {
|
|
640
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
641
|
+
isError: true
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
var ToolManager = class {
|
|
645
|
+
tools = /* @__PURE__ */ new Map();
|
|
646
|
+
onToolsChanged;
|
|
647
|
+
/**
|
|
648
|
+
* 创建工具管理器实例
|
|
649
|
+
*
|
|
650
|
+
* @param onToolsChanged - 可选的回调函数,当工具列表变化时调用
|
|
651
|
+
*/
|
|
652
|
+
constructor(onToolsChanged) {
|
|
653
|
+
this.onToolsChanged = onToolsChanged;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 注册新工具
|
|
657
|
+
*
|
|
658
|
+
* @param name - 工具名称,必须唯一
|
|
659
|
+
* @param description - 工具描述,供 AI 理解工具用途
|
|
660
|
+
* @param inputSchema - 参数 Schema,定义工具接受的参数
|
|
661
|
+
* @param handler - 处理函数,实际执行工具逻辑
|
|
662
|
+
*/
|
|
663
|
+
registerTool(name, description, inputSchema, handler) {
|
|
664
|
+
this.tools.set(name, {
|
|
665
|
+
name,
|
|
666
|
+
description,
|
|
667
|
+
inputSchema,
|
|
668
|
+
handler
|
|
669
|
+
});
|
|
670
|
+
this.onToolsChanged?.();
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* 注销工具
|
|
674
|
+
*
|
|
675
|
+
* @param name - 要注销的工具名称
|
|
676
|
+
* @returns 是否成功注销(false 表示工具不存在)
|
|
677
|
+
*/
|
|
678
|
+
unregisterTool(name) {
|
|
679
|
+
const result = this.tools.delete(name);
|
|
680
|
+
if (result) {
|
|
681
|
+
this.onToolsChanged?.();
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* 列出所有已注册的工具
|
|
687
|
+
*
|
|
688
|
+
* 返回工具的元信息(不包含处理函数),供 AI 发现可用工具。
|
|
689
|
+
*
|
|
690
|
+
* @returns 工具定义数组
|
|
691
|
+
*/
|
|
692
|
+
listTools() {
|
|
693
|
+
return Array.from(this.tools.values()).map(({ name, description, inputSchema }) => ({
|
|
694
|
+
name,
|
|
695
|
+
description,
|
|
696
|
+
inputSchema
|
|
697
|
+
}));
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* 按名称调用工具
|
|
701
|
+
*
|
|
702
|
+
* 查找并执行指定的工具,自动处理异常并返回错误结果。
|
|
703
|
+
*
|
|
704
|
+
* @param name - 工具名称
|
|
705
|
+
* @param params - 调用参数
|
|
706
|
+
* @returns 工具执行结果
|
|
707
|
+
*/
|
|
708
|
+
async callTool(name, params) {
|
|
709
|
+
const tool = this.tools.get(name);
|
|
710
|
+
if (!tool) {
|
|
711
|
+
return createToolError(`\u5DE5\u5177\u672A\u627E\u5230: ${name}`);
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
return await tool.handler(params);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
const message = err instanceof Error ? err.message : "\u672A\u77E5\u9519\u8BEF";
|
|
717
|
+
return createToolError(message);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* 检查工具是否已注册
|
|
722
|
+
*/
|
|
723
|
+
hasTool(name) {
|
|
724
|
+
return this.tools.has(name);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* 获取已注册的工具数量
|
|
728
|
+
*/
|
|
729
|
+
get toolCount() {
|
|
730
|
+
return this.tools.size;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
function registerToolHandlers(jsonRpcHandler, toolManager) {
|
|
734
|
+
jsonRpcHandler.registerRequestHandler("tools/list", async () => {
|
|
735
|
+
const tools = toolManager.listTools();
|
|
736
|
+
return { tools };
|
|
737
|
+
});
|
|
738
|
+
jsonRpcHandler.registerRequestHandler("tools/call", async (params) => {
|
|
739
|
+
const name = params?.name;
|
|
740
|
+
const args = params?.arguments ?? {};
|
|
741
|
+
if (!name) {
|
|
742
|
+
return createToolError("\u7F3A\u5C11\u5DE5\u5177\u540D\u79F0");
|
|
743
|
+
}
|
|
744
|
+
return toolManager.callTool(name, args);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/session-manager.ts
|
|
749
|
+
import { v4 as uuidv42 } from "uuid";
|
|
750
|
+
var SESSION_TTL_MS = 60 * 60 * 1e3;
|
|
751
|
+
var CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
752
|
+
var SessionManager = class {
|
|
753
|
+
sessions = /* @__PURE__ */ new Map();
|
|
754
|
+
cleanupTimer = null;
|
|
755
|
+
/** 连接超时时间(毫秒) */
|
|
756
|
+
connectionTimeoutMs;
|
|
757
|
+
constructor(connectionTimeoutMinutes = 5) {
|
|
758
|
+
this.connectionTimeoutMs = connectionTimeoutMinutes * 60 * 1e3;
|
|
759
|
+
this.startCleanupTimer();
|
|
760
|
+
console.log(`[\u4F1A\u8BDD] \u8FDE\u63A5\u8D85\u65F6\u8BBE\u7F6E: ${connectionTimeoutMinutes} \u5206\u949F`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 创建新的设备会话
|
|
764
|
+
*
|
|
765
|
+
* 生成唯一的 deviceId 并初始化会话状态。
|
|
766
|
+
* 新会话默认未连接、未绑定,强度为 0,上限为 200。
|
|
767
|
+
* 会启动连接超时计时器,如果在超时时间内未绑定 APP 则自动销毁。
|
|
768
|
+
*
|
|
769
|
+
* @returns 新创建的会话对象
|
|
770
|
+
*/
|
|
771
|
+
createSession() {
|
|
772
|
+
const deviceId = uuidv42();
|
|
773
|
+
const now = /* @__PURE__ */ new Date();
|
|
774
|
+
const session = {
|
|
775
|
+
deviceId,
|
|
776
|
+
alias: null,
|
|
777
|
+
clientId: null,
|
|
778
|
+
targetId: null,
|
|
779
|
+
ws: null,
|
|
780
|
+
connected: false,
|
|
781
|
+
boundToApp: false,
|
|
782
|
+
strengthA: 0,
|
|
783
|
+
strengthB: 0,
|
|
784
|
+
strengthLimitA: 200,
|
|
785
|
+
strengthLimitB: 200,
|
|
786
|
+
lastActive: now,
|
|
787
|
+
createdAt: now,
|
|
788
|
+
connectionTimeoutId: null
|
|
789
|
+
};
|
|
790
|
+
session.connectionTimeoutId = setTimeout(() => {
|
|
791
|
+
const currentSession = this.sessions.get(deviceId);
|
|
792
|
+
if (currentSession && !currentSession.boundToApp) {
|
|
793
|
+
console.log(`[\u4F1A\u8BDD] \u8FDE\u63A5\u8D85\u65F6: ${deviceId} (${this.connectionTimeoutMs / 6e4} \u5206\u949F\u5185\u672A\u7ED1\u5B9A APP)`);
|
|
794
|
+
this.deleteSession(deviceId);
|
|
795
|
+
}
|
|
796
|
+
}, this.connectionTimeoutMs);
|
|
797
|
+
this.sessions.set(deviceId, session);
|
|
798
|
+
console.log(`[\u4F1A\u8BDD] \u5DF2\u521B\u5EFA: ${deviceId}`);
|
|
799
|
+
return session;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* 根据 deviceId 获取会话
|
|
803
|
+
*
|
|
804
|
+
* 如果会话已过期,会自动删除并返回 null。
|
|
805
|
+
*
|
|
806
|
+
* @param deviceId - 设备 ID
|
|
807
|
+
* @returns 会话对象,如果不存在或已过期则返回 null
|
|
808
|
+
*/
|
|
809
|
+
getSession(deviceId) {
|
|
810
|
+
const session = this.sessions.get(deviceId);
|
|
811
|
+
if (session) {
|
|
812
|
+
if (this.isExpired(session)) {
|
|
813
|
+
this.deleteSession(deviceId);
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return session ?? null;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* 根据 clientId 获取会话
|
|
821
|
+
*
|
|
822
|
+
* clientId 是 WebSocket 服务器分配的标识符,用于关联 MCP 会话和 WS 连接。
|
|
823
|
+
*
|
|
824
|
+
* @param clientId - WebSocket 客户端 ID
|
|
825
|
+
* @returns 会话对象,如果不存在或已过期则返回 null
|
|
826
|
+
*/
|
|
827
|
+
getSessionByClientId(clientId) {
|
|
828
|
+
for (const session of this.sessions.values()) {
|
|
829
|
+
if (session.clientId === clientId && !this.isExpired(session)) {
|
|
830
|
+
return session;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* 列出所有活跃会话
|
|
837
|
+
*
|
|
838
|
+
* 返回前会先清理过期会话,确保返回的都是有效会话。
|
|
839
|
+
*
|
|
840
|
+
* @returns 所有活跃会话的数组
|
|
841
|
+
*/
|
|
842
|
+
listSessions() {
|
|
843
|
+
this.cleanupExpiredSessions();
|
|
844
|
+
return Array.from(this.sessions.values());
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* 删除会话
|
|
848
|
+
*
|
|
849
|
+
* 会关闭关联的 WebSocket 连接、清理超时计时器并从内存中移除会话数据。
|
|
850
|
+
*
|
|
851
|
+
* @param deviceId - 要删除的设备 ID
|
|
852
|
+
* @returns 是否成功删除(false 表示会话不存在)
|
|
853
|
+
*/
|
|
854
|
+
deleteSession(deviceId) {
|
|
855
|
+
const session = this.sessions.get(deviceId);
|
|
856
|
+
if (session) {
|
|
857
|
+
if (session.connectionTimeoutId) {
|
|
858
|
+
clearTimeout(session.connectionTimeoutId);
|
|
859
|
+
session.connectionTimeoutId = null;
|
|
860
|
+
}
|
|
861
|
+
if (session.ws) {
|
|
862
|
+
try {
|
|
863
|
+
session.ws.close();
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
this.sessions.delete(deviceId);
|
|
868
|
+
console.log(`[\u4F1A\u8BDD] \u5DF2\u5220\u9664: ${deviceId}`);
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* 设置设备别名
|
|
875
|
+
*
|
|
876
|
+
* 别名用于方便识别设备,支持中文和特殊字符。
|
|
877
|
+
* 设置别名会同时更新会话的活跃时间。
|
|
878
|
+
*
|
|
879
|
+
* @param deviceId - 设备 ID
|
|
880
|
+
* @param alias - 新的别名
|
|
881
|
+
* @returns 是否成功设置(false 表示设备不存在或已过期)
|
|
882
|
+
*/
|
|
883
|
+
setAlias(deviceId, alias) {
|
|
884
|
+
const session = this.sessions.get(deviceId);
|
|
885
|
+
if (session && !this.isExpired(session)) {
|
|
886
|
+
session.alias = alias;
|
|
887
|
+
session.lastActive = /* @__PURE__ */ new Date();
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* 根据别名查找会话
|
|
894
|
+
*
|
|
895
|
+
* 支持大小写不敏感的精确匹配。
|
|
896
|
+
*
|
|
897
|
+
* @param alias - 要查找的别名
|
|
898
|
+
* @returns 所有匹配的会话数组
|
|
899
|
+
*/
|
|
900
|
+
findByAlias(alias) {
|
|
901
|
+
const lowerAlias = alias.toLowerCase();
|
|
902
|
+
return Array.from(this.sessions.values()).filter(
|
|
903
|
+
(s) => !this.isExpired(s) && s.alias?.toLowerCase() === lowerAlias
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* 更新会话连接状态
|
|
908
|
+
*
|
|
909
|
+
* 用于在 WebSocket 连接建立或断开时更新会话状态。
|
|
910
|
+
*
|
|
911
|
+
* @param deviceId - 设备 ID
|
|
912
|
+
* @param updates - 要更新的字段
|
|
913
|
+
* @returns 是否成功更新
|
|
914
|
+
*/
|
|
915
|
+
updateConnectionState(deviceId, updates) {
|
|
916
|
+
const session = this.sessions.get(deviceId);
|
|
917
|
+
if (session) {
|
|
918
|
+
Object.assign(session, updates);
|
|
919
|
+
session.lastActive = /* @__PURE__ */ new Date();
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* 更新会话强度值
|
|
926
|
+
*
|
|
927
|
+
* 当收到 APP 的强度反馈时调用,同步更新会话中的强度数据。
|
|
928
|
+
*
|
|
929
|
+
* @param deviceId - 设备 ID
|
|
930
|
+
* @param strengthA - A 通道当前强度
|
|
931
|
+
* @param strengthB - B 通道当前强度
|
|
932
|
+
* @param strengthLimitA - A 通道强度上限
|
|
933
|
+
* @param strengthLimitB - B 通道强度上限
|
|
934
|
+
* @returns 是否成功更新
|
|
935
|
+
*/
|
|
936
|
+
updateStrength(deviceId, strengthA, strengthB, strengthLimitA, strengthLimitB) {
|
|
937
|
+
const session = this.sessions.get(deviceId);
|
|
938
|
+
if (session) {
|
|
939
|
+
session.strengthA = strengthA;
|
|
940
|
+
session.strengthB = strengthB;
|
|
941
|
+
session.strengthLimitA = strengthLimitA;
|
|
942
|
+
session.strengthLimitB = strengthLimitB;
|
|
943
|
+
session.lastActive = /* @__PURE__ */ new Date();
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* 触摸会话以保持活跃
|
|
950
|
+
*
|
|
951
|
+
* 更新 lastActive 时间戳,防止会话因不活动而过期。
|
|
952
|
+
* 每次对设备的操作都应该调用此方法。
|
|
953
|
+
*
|
|
954
|
+
* @param deviceId - 设备 ID
|
|
955
|
+
* @returns 是否成功
|
|
956
|
+
*/
|
|
957
|
+
touchSession(deviceId) {
|
|
958
|
+
const session = this.sessions.get(deviceId);
|
|
959
|
+
if (session) {
|
|
960
|
+
session.lastActive = /* @__PURE__ */ new Date();
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* APP 绑定成功时调用
|
|
967
|
+
*
|
|
968
|
+
* 取消连接超时计时器,因为设备已成功绑定 APP。
|
|
969
|
+
* 同时更新会话的 boundToApp 状态。
|
|
970
|
+
*
|
|
971
|
+
* @param deviceId - 设备 ID
|
|
972
|
+
* @returns 是否成功
|
|
973
|
+
*/
|
|
974
|
+
onAppBound(deviceId) {
|
|
975
|
+
const session = this.sessions.get(deviceId);
|
|
976
|
+
if (session) {
|
|
977
|
+
if (session.connectionTimeoutId) {
|
|
978
|
+
clearTimeout(session.connectionTimeoutId);
|
|
979
|
+
session.connectionTimeoutId = null;
|
|
980
|
+
console.log(`[\u4F1A\u8BDD] \u5DF2\u53D6\u6D88\u8FDE\u63A5\u8D85\u65F6: ${deviceId} (APP \u5DF2\u7ED1\u5B9A)`);
|
|
981
|
+
}
|
|
982
|
+
session.boundToApp = true;
|
|
983
|
+
session.lastActive = /* @__PURE__ */ new Date();
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* 获取当前会话数量
|
|
990
|
+
*/
|
|
991
|
+
get sessionCount() {
|
|
992
|
+
return this.sessions.size;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* 检查会话是否已过期
|
|
996
|
+
*/
|
|
997
|
+
isExpired(session) {
|
|
998
|
+
return Date.now() - session.lastActive.getTime() > SESSION_TTL_MS;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* 清理所有过期会话
|
|
1002
|
+
*
|
|
1003
|
+
* 遍历所有会话,删除超过 TTL 的会话并关闭其 WebSocket 连接。
|
|
1004
|
+
*
|
|
1005
|
+
* @returns 清理的会话数量
|
|
1006
|
+
*/
|
|
1007
|
+
cleanupExpiredSessions() {
|
|
1008
|
+
let cleaned = 0;
|
|
1009
|
+
const now = Date.now();
|
|
1010
|
+
for (const [deviceId, session] of this.sessions) {
|
|
1011
|
+
const age = now - session.lastActive.getTime();
|
|
1012
|
+
if (age > SESSION_TTL_MS) {
|
|
1013
|
+
if (session.connectionTimeoutId) {
|
|
1014
|
+
clearTimeout(session.connectionTimeoutId);
|
|
1015
|
+
}
|
|
1016
|
+
if (session.ws) {
|
|
1017
|
+
try {
|
|
1018
|
+
session.ws.close();
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
this.sessions.delete(deviceId);
|
|
1023
|
+
console.log(`[\u4F1A\u8BDD] \u5DF2\u8FC7\u671F: ${deviceId} (\u4E0D\u6D3B\u8DC3 ${Math.round(age / 6e4)} \u5206\u949F)`);
|
|
1024
|
+
cleaned++;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return cleaned;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* 启动定期清理定时器
|
|
1031
|
+
*/
|
|
1032
|
+
startCleanupTimer() {
|
|
1033
|
+
this.cleanupTimer = setInterval(() => {
|
|
1034
|
+
const cleaned = this.cleanupExpiredSessions();
|
|
1035
|
+
if (cleaned > 0) {
|
|
1036
|
+
console.log(`[\u4F1A\u8BDD] \u6E05\u7406: ${cleaned} \u4E2A\u8FC7\u671F\uFF0C\u5269\u4F59 ${this.sessions.size} \u4E2A`);
|
|
1037
|
+
}
|
|
1038
|
+
}, CLEANUP_INTERVAL_MS);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* 停止定期清理定时器
|
|
1042
|
+
*
|
|
1043
|
+
* 在服务器关闭时调用,防止内存泄漏。
|
|
1044
|
+
*/
|
|
1045
|
+
stopCleanupTimer() {
|
|
1046
|
+
if (this.cleanupTimer) {
|
|
1047
|
+
clearInterval(this.cleanupTimer);
|
|
1048
|
+
this.cleanupTimer = null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* 清除所有会话
|
|
1053
|
+
*
|
|
1054
|
+
* 关闭所有 WebSocket 连接、清理所有计时器并清空会话存储。
|
|
1055
|
+
* 通常在服务器关闭或重置时调用。
|
|
1056
|
+
*/
|
|
1057
|
+
clearAll() {
|
|
1058
|
+
for (const session of this.sessions.values()) {
|
|
1059
|
+
if (session.connectionTimeoutId) {
|
|
1060
|
+
clearTimeout(session.connectionTimeoutId);
|
|
1061
|
+
}
|
|
1062
|
+
if (session.ws) {
|
|
1063
|
+
try {
|
|
1064
|
+
session.ws.close();
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
this.sessions.clear();
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// src/ws-server.ts
|
|
1074
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
1075
|
+
import { v4 as uuidv43 } from "uuid";
|
|
1076
|
+
|
|
1077
|
+
// src/ws-bridge.ts
|
|
1078
|
+
import WebSocket from "ws";
|
|
1079
|
+
|
|
1080
|
+
// src/ws-server.ts
|
|
1081
|
+
var DGLabWSServer = class {
|
|
1082
|
+
wss = null;
|
|
1083
|
+
clients = /* @__PURE__ */ new Map();
|
|
1084
|
+
relations = /* @__PURE__ */ new Map();
|
|
1085
|
+
waveformTimers = /* @__PURE__ */ new Map();
|
|
1086
|
+
/** 持续播放状态 Map,key 格式: controllerId-channel */
|
|
1087
|
+
continuousPlaybacks = /* @__PURE__ */ new Map();
|
|
1088
|
+
heartbeatTimer = null;
|
|
1089
|
+
options;
|
|
1090
|
+
attachedPort = 0;
|
|
1091
|
+
constructor(options) {
|
|
1092
|
+
this.options = {
|
|
1093
|
+
heartbeatInterval: 6e4,
|
|
1094
|
+
onStrengthUpdate: () => {
|
|
1095
|
+
},
|
|
1096
|
+
onFeedback: () => {
|
|
1097
|
+
},
|
|
1098
|
+
onBindChange: () => {
|
|
1099
|
+
},
|
|
1100
|
+
...options
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/** 启动独立的 WebSocket 服务器(使用独立端口) */
|
|
1104
|
+
start() {
|
|
1105
|
+
if (!this.options.port) {
|
|
1106
|
+
throw new Error("\u72EC\u7ACB\u542F\u52A8\u9700\u8981\u6307\u5B9A port");
|
|
1107
|
+
}
|
|
1108
|
+
this.wss = new WebSocketServer({ port: this.options.port });
|
|
1109
|
+
this.wss.on("connection", (ws, req) => {
|
|
1110
|
+
this.handleConnection(ws, req);
|
|
1111
|
+
});
|
|
1112
|
+
this.startHeartbeat();
|
|
1113
|
+
this.attachedPort = this.options.port;
|
|
1114
|
+
console.log(`[WS \u670D\u52A1\u5668] \u72EC\u7ACB\u76D1\u542C\u7AEF\u53E3 ${this.options.port}`);
|
|
1115
|
+
}
|
|
1116
|
+
/** 附加到现有的 HTTP 服务器(共享端口) */
|
|
1117
|
+
attachToServer(httpServer, port) {
|
|
1118
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
1119
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
1120
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1121
|
+
this.wss.emit("connection", ws, request);
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
this.wss.on("connection", (ws, req) => {
|
|
1125
|
+
this.handleConnection(ws, req);
|
|
1126
|
+
});
|
|
1127
|
+
this.startHeartbeat();
|
|
1128
|
+
this.attachedPort = port;
|
|
1129
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u9644\u52A0\u5230 HTTP \u670D\u52A1\u5668\uFF0C\u5171\u4EAB\u7AEF\u53E3 ${port}`);
|
|
1130
|
+
}
|
|
1131
|
+
/** 启动心跳定时器 */
|
|
1132
|
+
startHeartbeat() {
|
|
1133
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1134
|
+
this.sendHeartbeats();
|
|
1135
|
+
}, this.options.heartbeatInterval);
|
|
1136
|
+
}
|
|
1137
|
+
/** 停止 WebSocket 服务器 */
|
|
1138
|
+
stop() {
|
|
1139
|
+
if (this.heartbeatTimer) {
|
|
1140
|
+
clearInterval(this.heartbeatTimer);
|
|
1141
|
+
this.heartbeatTimer = null;
|
|
1142
|
+
}
|
|
1143
|
+
for (const timer of this.waveformTimers.values()) {
|
|
1144
|
+
clearInterval(timer.timerId);
|
|
1145
|
+
}
|
|
1146
|
+
this.waveformTimers.clear();
|
|
1147
|
+
for (const playback of this.continuousPlaybacks.values()) {
|
|
1148
|
+
if (playback.timerId) {
|
|
1149
|
+
clearInterval(playback.timerId);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
this.continuousPlaybacks.clear();
|
|
1153
|
+
for (const client of this.clients.values()) {
|
|
1154
|
+
client.ws.close();
|
|
1155
|
+
}
|
|
1156
|
+
this.clients.clear();
|
|
1157
|
+
this.relations.clear();
|
|
1158
|
+
if (this.wss) {
|
|
1159
|
+
this.wss.close();
|
|
1160
|
+
this.wss = null;
|
|
1161
|
+
}
|
|
1162
|
+
console.log("[WS \u670D\u52A1\u5668] \u5DF2\u505C\u6B62");
|
|
1163
|
+
}
|
|
1164
|
+
/** 处理新的 WebSocket 连接 */
|
|
1165
|
+
handleConnection(ws, _req) {
|
|
1166
|
+
const clientId = uuidv43();
|
|
1167
|
+
const clientInfo = {
|
|
1168
|
+
id: clientId,
|
|
1169
|
+
ws,
|
|
1170
|
+
type: "unknown",
|
|
1171
|
+
boundTo: null,
|
|
1172
|
+
lastActive: Date.now()
|
|
1173
|
+
};
|
|
1174
|
+
this.clients.set(clientId, clientInfo);
|
|
1175
|
+
console.log(`[WS \u670D\u52A1\u5668] \u65B0\u8FDE\u63A5: ${clientId}`);
|
|
1176
|
+
this.send(ws, { type: "bind", clientId, targetId: "", message: "targetId" });
|
|
1177
|
+
ws.on("message", (data) => this.handleMessage(clientId, data.toString()));
|
|
1178
|
+
ws.on("close", () => this.handleClose(clientId));
|
|
1179
|
+
ws.on("error", (error) => {
|
|
1180
|
+
console.error(`[WS \u670D\u52A1\u5668] \u9519\u8BEF ${clientId}:`, error.message);
|
|
1181
|
+
this.handleError(clientId, error);
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/** 处理收到的消息 */
|
|
1185
|
+
handleMessage(clientId, rawData) {
|
|
1186
|
+
console.log(`[WS \u670D\u52A1\u5668] \u6536\u5230 ${clientId}: ${rawData}`);
|
|
1187
|
+
const client = this.clients.get(clientId);
|
|
1188
|
+
if (!client) return;
|
|
1189
|
+
client.lastActive = Date.now();
|
|
1190
|
+
let data;
|
|
1191
|
+
try {
|
|
1192
|
+
data = JSON.parse(rawData);
|
|
1193
|
+
} catch {
|
|
1194
|
+
this.send(client.ws, { type: "msg", clientId: "", targetId: "", message: "403" });
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (data.clientId !== clientId && data.targetId !== clientId) {
|
|
1198
|
+
if (!(data.type === "bind" && data.message === "DGLAB")) {
|
|
1199
|
+
this.send(client.ws, { type: "msg", clientId: "", targetId: "", message: "404" });
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
switch (data.type) {
|
|
1204
|
+
case "bind":
|
|
1205
|
+
this.handleBind(clientId, data);
|
|
1206
|
+
break;
|
|
1207
|
+
case "msg":
|
|
1208
|
+
this.handleMsg(clientId, data);
|
|
1209
|
+
break;
|
|
1210
|
+
case "heartbeat":
|
|
1211
|
+
break;
|
|
1212
|
+
default:
|
|
1213
|
+
this.forwardMessage(clientId, data);
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/** 处理绑定请求 */
|
|
1218
|
+
handleBind(clientId, data) {
|
|
1219
|
+
const client = this.clients.get(clientId);
|
|
1220
|
+
if (!client) return;
|
|
1221
|
+
if (data.message === "DGLAB" && data.clientId && data.targetId) {
|
|
1222
|
+
const controllerId = data.clientId;
|
|
1223
|
+
const appId = data.targetId;
|
|
1224
|
+
if (!this.clients.has(controllerId) || !this.clients.has(appId)) {
|
|
1225
|
+
this.send(client.ws, { type: "bind", clientId: controllerId, targetId: appId, message: "401" });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const alreadyBound = [controllerId, appId].some(
|
|
1229
|
+
(id) => this.relations.has(id) || [...this.relations.values()].includes(id)
|
|
1230
|
+
);
|
|
1231
|
+
if (alreadyBound) {
|
|
1232
|
+
this.send(client.ws, { type: "bind", clientId: controllerId, targetId: appId, message: "400" });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
this.relations.set(controllerId, appId);
|
|
1236
|
+
const controllerClient = this.clients.get(controllerId);
|
|
1237
|
+
const appClient = this.clients.get(appId);
|
|
1238
|
+
if (controllerClient) {
|
|
1239
|
+
controllerClient.type = "controller";
|
|
1240
|
+
controllerClient.boundTo = appId;
|
|
1241
|
+
}
|
|
1242
|
+
if (appClient) {
|
|
1243
|
+
appClient.type = "app";
|
|
1244
|
+
appClient.boundTo = controllerId;
|
|
1245
|
+
}
|
|
1246
|
+
const successMsg = { type: "bind", clientId: controllerId, targetId: appId, message: "200" };
|
|
1247
|
+
if (controllerClient) this.send(controllerClient.ws, successMsg);
|
|
1248
|
+
if (appClient) this.send(appClient.ws, successMsg);
|
|
1249
|
+
if (this.options.onBindChange) {
|
|
1250
|
+
this.options.onBindChange(controllerId, appId);
|
|
1251
|
+
}
|
|
1252
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u7ED1\u5B9A: ${controllerId} <-> ${appId}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/** 处理 msg 类型消息 */
|
|
1256
|
+
handleMsg(clientId, data) {
|
|
1257
|
+
const { message } = data;
|
|
1258
|
+
if (message.startsWith("strength-")) {
|
|
1259
|
+
const parsed = this.parseStrengthMessage(message);
|
|
1260
|
+
if (parsed) {
|
|
1261
|
+
const client = this.clients.get(clientId);
|
|
1262
|
+
if (client?.boundTo && this.options.onStrengthUpdate) {
|
|
1263
|
+
this.options.onStrengthUpdate(client.boundTo, parsed.strengthA, parsed.strengthB, parsed.limitA, parsed.limitB);
|
|
1264
|
+
}
|
|
1265
|
+
this.forwardMessage(clientId, data);
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (message.startsWith("feedback-")) {
|
|
1270
|
+
const index = parseInt(message.substring(9));
|
|
1271
|
+
if (!isNaN(index)) {
|
|
1272
|
+
const client = this.clients.get(clientId);
|
|
1273
|
+
if (client?.boundTo && this.options.onFeedback) {
|
|
1274
|
+
this.options.onFeedback(client.boundTo, index);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
this.forwardMessage(clientId, data);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
this.forwardMessage(clientId, data);
|
|
1281
|
+
}
|
|
1282
|
+
/** 转发消息给绑定的对方 */
|
|
1283
|
+
forwardMessage(fromClientId, data) {
|
|
1284
|
+
const client = this.clients.get(fromClientId);
|
|
1285
|
+
if (!client?.boundTo) return;
|
|
1286
|
+
const boundId = this.relations.get(fromClientId) || [...this.relations.entries()].find(([_, v]) => v === fromClientId)?.[0];
|
|
1287
|
+
if (!boundId) {
|
|
1288
|
+
this.send(client.ws, { type: "bind", clientId: data.clientId, targetId: data.targetId, message: "402" });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const targetClient = this.clients.get(client.boundTo);
|
|
1292
|
+
if (targetClient) {
|
|
1293
|
+
this.send(targetClient.ws, data);
|
|
1294
|
+
} else {
|
|
1295
|
+
this.send(client.ws, { type: "msg", clientId: data.clientId, targetId: data.targetId, message: "404" });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
/** 处理客户端断开 */
|
|
1299
|
+
handleClose(clientId) {
|
|
1300
|
+
console.log(`[WS \u670D\u52A1\u5668] \u65AD\u5F00: ${clientId}`);
|
|
1301
|
+
const client = this.clients.get(clientId);
|
|
1302
|
+
if (!client) return;
|
|
1303
|
+
for (const [key, timer] of this.waveformTimers.entries()) {
|
|
1304
|
+
if (key.startsWith(clientId + "-")) {
|
|
1305
|
+
clearInterval(timer.timerId);
|
|
1306
|
+
this.waveformTimers.delete(key);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
for (const [key, playback] of this.continuousPlaybacks.entries()) {
|
|
1310
|
+
if (playback.controllerId === clientId) {
|
|
1311
|
+
if (playback.timerId) {
|
|
1312
|
+
clearInterval(playback.timerId);
|
|
1313
|
+
}
|
|
1314
|
+
this.continuousPlaybacks.delete(key);
|
|
1315
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u505C\u6B62\u6301\u7EED\u64AD\u653E: ${key}`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (client.type === "app") {
|
|
1319
|
+
for (const [controllerId, appId] of this.relations.entries()) {
|
|
1320
|
+
if (appId === clientId) {
|
|
1321
|
+
const controller = this.clients.get(controllerId);
|
|
1322
|
+
if (controller) {
|
|
1323
|
+
this.send(controller.ws, {
|
|
1324
|
+
type: "break",
|
|
1325
|
+
clientId: controllerId,
|
|
1326
|
+
targetId: clientId,
|
|
1327
|
+
message: "209"
|
|
1328
|
+
});
|
|
1329
|
+
controller.boundTo = null;
|
|
1330
|
+
}
|
|
1331
|
+
this.relations.delete(controllerId);
|
|
1332
|
+
if (this.options.onBindChange) {
|
|
1333
|
+
this.options.onBindChange(controllerId, null);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (this.options.onAppDisconnect) {
|
|
1338
|
+
this.options.onAppDisconnect(clientId);
|
|
1339
|
+
}
|
|
1340
|
+
} else if (client.type === "controller") {
|
|
1341
|
+
if (client.boundTo) {
|
|
1342
|
+
const partner = this.clients.get(client.boundTo);
|
|
1343
|
+
if (partner) {
|
|
1344
|
+
this.send(partner.ws, { type: "break", clientId: client.boundTo, targetId: clientId, message: "209" });
|
|
1345
|
+
partner.boundTo = null;
|
|
1346
|
+
}
|
|
1347
|
+
this.relations.delete(clientId);
|
|
1348
|
+
if (this.options.onBindChange) {
|
|
1349
|
+
this.options.onBindChange(clientId, null);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (this.options.onControllerDisconnect) {
|
|
1353
|
+
this.options.onControllerDisconnect(clientId);
|
|
1354
|
+
}
|
|
1355
|
+
} else {
|
|
1356
|
+
if (client.boundTo) {
|
|
1357
|
+
const partner = this.clients.get(client.boundTo);
|
|
1358
|
+
if (partner) {
|
|
1359
|
+
this.send(partner.ws, { type: "break", clientId: client.boundTo, targetId: clientId, message: "209" });
|
|
1360
|
+
partner.boundTo = null;
|
|
1361
|
+
}
|
|
1362
|
+
this.relations.delete(clientId);
|
|
1363
|
+
this.relations.delete(client.boundTo);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
this.clients.delete(clientId);
|
|
1367
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u6E05\u7406 ${clientId}\uFF0C\u5BA2\u6237\u7AEF\u6570: ${this.clients.size}`);
|
|
1368
|
+
}
|
|
1369
|
+
/** 处理客户端错误 */
|
|
1370
|
+
handleError(clientId, error) {
|
|
1371
|
+
const client = this.clients.get(clientId);
|
|
1372
|
+
if (!client?.boundTo) return;
|
|
1373
|
+
const partner = this.clients.get(client.boundTo);
|
|
1374
|
+
if (partner) {
|
|
1375
|
+
this.send(partner.ws, { type: "error", clientId: client.boundTo, targetId: clientId, message: "500" });
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/** 发送心跳给所有客户端 */
|
|
1379
|
+
sendHeartbeats() {
|
|
1380
|
+
if (this.clients.size === 0) return;
|
|
1381
|
+
console.log(`[WS \u670D\u52A1\u5668] \u53D1\u9001\u5FC3\u8DF3\u7ED9 ${this.clients.size} \u4E2A\u5BA2\u6237\u7AEF`);
|
|
1382
|
+
for (const [clientId, client] of this.clients.entries()) {
|
|
1383
|
+
this.send(client.ws, { type: "heartbeat", clientId, targetId: client.boundTo || "", message: "200" });
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/** 发送消息到 WebSocket */
|
|
1387
|
+
send(ws, msg) {
|
|
1388
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1389
|
+
ws.send(JSON.stringify(msg));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/** 解析强度消息 */
|
|
1393
|
+
parseStrengthMessage(message) {
|
|
1394
|
+
const match = message.match(/^strength-(\d+)\+(\d+)\+(\d+)\+(\d+)$/);
|
|
1395
|
+
if (!match) return null;
|
|
1396
|
+
return { strengthA: parseInt(match[1], 10), strengthB: parseInt(match[2], 10), limitA: parseInt(match[3], 10), limitB: parseInt(match[4], 10) };
|
|
1397
|
+
}
|
|
1398
|
+
// ============ MCP 工具公共 API ============
|
|
1399
|
+
/** 创建控制器(用于 dg_connect) */
|
|
1400
|
+
createController() {
|
|
1401
|
+
const clientId = uuidv43();
|
|
1402
|
+
const mockWs = this.createMockWebSocket(clientId);
|
|
1403
|
+
const clientInfo = { id: clientId, ws: mockWs, type: "controller", boundTo: null, lastActive: Date.now() };
|
|
1404
|
+
this.clients.set(clientId, clientInfo);
|
|
1405
|
+
console.log(`[WS \u670D\u52A1\u5668] \u521B\u5EFA\u63A7\u5236\u5668: ${clientId}`);
|
|
1406
|
+
return clientId;
|
|
1407
|
+
}
|
|
1408
|
+
/** 创建内部控制器的模拟 WebSocket */
|
|
1409
|
+
createMockWebSocket(clientId) {
|
|
1410
|
+
return {
|
|
1411
|
+
readyState: WebSocket2.OPEN,
|
|
1412
|
+
send: (data) => {
|
|
1413
|
+
console.log(`[WS \u670D\u52A1\u5668] \u53D1\u9001\u7ED9\u63A7\u5236\u5668 ${clientId}: ${data}`);
|
|
1414
|
+
},
|
|
1415
|
+
close: () => {
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
/** 检查控制器是否已绑定 APP */
|
|
1420
|
+
isControllerBound(controllerId) {
|
|
1421
|
+
return this.relations.has(controllerId);
|
|
1422
|
+
}
|
|
1423
|
+
/** 获取绑定到控制器的 APP clientId */
|
|
1424
|
+
getBoundAppId(controllerId) {
|
|
1425
|
+
return this.relations.get(controllerId) || null;
|
|
1426
|
+
}
|
|
1427
|
+
/** 获取控制器信息 */
|
|
1428
|
+
getController(controllerId) {
|
|
1429
|
+
const client = this.clients.get(controllerId);
|
|
1430
|
+
return client?.type === "controller" ? client : null;
|
|
1431
|
+
}
|
|
1432
|
+
/** 列出所有控制器 */
|
|
1433
|
+
listControllers() {
|
|
1434
|
+
const result = [];
|
|
1435
|
+
for (const client of this.clients.values()) {
|
|
1436
|
+
if (client.type === "controller") {
|
|
1437
|
+
result.push({ id: client.id, boundTo: client.boundTo, lastActive: client.lastActive });
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return result;
|
|
1441
|
+
}
|
|
1442
|
+
/** 移除控制器 */
|
|
1443
|
+
removeController(controllerId) {
|
|
1444
|
+
const client = this.clients.get(controllerId);
|
|
1445
|
+
if (!client || client.type !== "controller") return false;
|
|
1446
|
+
this.handleClose(controllerId);
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* 断开指定控制器的连接
|
|
1451
|
+
* @param controllerId - 控制器 ID
|
|
1452
|
+
* @returns 是否成功断开
|
|
1453
|
+
*/
|
|
1454
|
+
disconnectController(controllerId) {
|
|
1455
|
+
const client = this.clients.get(controllerId);
|
|
1456
|
+
if (!client) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
for (const [key, timer] of this.waveformTimers.entries()) {
|
|
1460
|
+
if (key.startsWith(controllerId + "-")) {
|
|
1461
|
+
clearInterval(timer.timerId);
|
|
1462
|
+
this.waveformTimers.delete(key);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
for (const channel of ["A", "B"]) {
|
|
1466
|
+
const key = `${controllerId}-${channel}`;
|
|
1467
|
+
const state = this.continuousPlaybacks.get(key);
|
|
1468
|
+
if (state) {
|
|
1469
|
+
if (state.timerId) {
|
|
1470
|
+
clearInterval(state.timerId);
|
|
1471
|
+
}
|
|
1472
|
+
this.continuousPlaybacks.delete(key);
|
|
1473
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u505C\u6B62\u6301\u7EED\u64AD\u653E: ${key}`);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (client.boundTo) {
|
|
1477
|
+
const appClient = this.clients.get(client.boundTo);
|
|
1478
|
+
if (appClient && appClient.ws.readyState === WebSocket2.OPEN) {
|
|
1479
|
+
this.send(appClient.ws, {
|
|
1480
|
+
type: "break",
|
|
1481
|
+
clientId: controllerId,
|
|
1482
|
+
targetId: client.boundTo,
|
|
1483
|
+
message: "209"
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
this.relations.delete(controllerId);
|
|
1487
|
+
if (appClient) {
|
|
1488
|
+
appClient.boundTo = null;
|
|
1489
|
+
}
|
|
1490
|
+
if (this.options.onBindChange) {
|
|
1491
|
+
this.options.onBindChange(controllerId, null);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (client.ws.readyState === WebSocket2.OPEN) {
|
|
1495
|
+
client.ws.close(1e3, "Disconnected by user");
|
|
1496
|
+
}
|
|
1497
|
+
this.clients.delete(controllerId);
|
|
1498
|
+
if (this.options.onControllerDisconnect) {
|
|
1499
|
+
this.options.onControllerDisconnect(controllerId);
|
|
1500
|
+
}
|
|
1501
|
+
console.log(`[WS \u670D\u52A1\u5668] \u63A7\u5236\u5668\u5DF2\u65AD\u5F00: ${controllerId}`);
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
/** 发送强度命令到 APP */
|
|
1505
|
+
sendStrength(controllerId, channel, mode, value) {
|
|
1506
|
+
const appId = this.relations.get(controllerId);
|
|
1507
|
+
if (!appId) return false;
|
|
1508
|
+
const appClient = this.clients.get(appId);
|
|
1509
|
+
if (!appClient) return false;
|
|
1510
|
+
const channelNum = channel === "A" ? 1 : 2;
|
|
1511
|
+
const modeNum = mode === "decrease" ? 0 : mode === "increase" ? 1 : 2;
|
|
1512
|
+
const message = `strength-${channelNum}+${modeNum}+${value}`;
|
|
1513
|
+
this.send(appClient.ws, { type: "msg", clientId: controllerId, targetId: appId, message });
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
/** 发送波形到 APP */
|
|
1517
|
+
sendWaveform(controllerId, channel, waveforms) {
|
|
1518
|
+
const appId = this.relations.get(controllerId);
|
|
1519
|
+
if (!appId) return false;
|
|
1520
|
+
const appClient = this.clients.get(appId);
|
|
1521
|
+
if (!appClient) return false;
|
|
1522
|
+
const message = `pulse-${channel}:${JSON.stringify(waveforms)}`;
|
|
1523
|
+
this.send(appClient.ws, { type: "msg", clientId: controllerId, targetId: appId, message });
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
/** 清空波形队列 */
|
|
1527
|
+
clearWaveform(controllerId, channel) {
|
|
1528
|
+
const appId = this.relations.get(controllerId);
|
|
1529
|
+
if (!appId) return false;
|
|
1530
|
+
const appClient = this.clients.get(appId);
|
|
1531
|
+
if (!appClient) return false;
|
|
1532
|
+
const channelNum = channel === "A" ? 1 : 2;
|
|
1533
|
+
this.send(appClient.ws, { type: "msg", clientId: controllerId, targetId: appId, message: `clear-${channelNum}` });
|
|
1534
|
+
return true;
|
|
1535
|
+
}
|
|
1536
|
+
// ============ 持续播放 API ============
|
|
1537
|
+
/**
|
|
1538
|
+
* 启动持续播放
|
|
1539
|
+
*
|
|
1540
|
+
* 循环发送波形数据到指定通道,直到手动停止。
|
|
1541
|
+
* 每次发送一批波形,按间隔循环发送。
|
|
1542
|
+
*
|
|
1543
|
+
* @param controllerId - 控制器 ID
|
|
1544
|
+
* @param channel - 目标通道 A 或 B
|
|
1545
|
+
* @param waveforms - 要循环播放的波形数据
|
|
1546
|
+
* @param interval - 发送间隔(毫秒),默认 100ms
|
|
1547
|
+
* @param batchSize - 每次发送的波形数量,默认 5
|
|
1548
|
+
* @returns 是否成功启动
|
|
1549
|
+
*/
|
|
1550
|
+
startContinuousPlayback(controllerId, channel, waveforms, interval = 100, batchSize = 5) {
|
|
1551
|
+
if (!this.isControllerBound(controllerId)) {
|
|
1552
|
+
console.log(`[WS \u670D\u52A1\u5668] \u6301\u7EED\u64AD\u653E\u5931\u8D25: \u63A7\u5236\u5668 ${controllerId} \u672A\u7ED1\u5B9A APP`);
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
if (!waveforms || waveforms.length === 0) {
|
|
1556
|
+
console.log(`[WS \u670D\u52A1\u5668] \u6301\u7EED\u64AD\u653E\u5931\u8D25: \u6CE2\u5F62\u6570\u636E\u4E3A\u7A7A`);
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
const key = `${controllerId}-${channel}`;
|
|
1560
|
+
if (this.continuousPlaybacks.has(key)) {
|
|
1561
|
+
this.stopContinuousPlayback(controllerId, channel);
|
|
1562
|
+
}
|
|
1563
|
+
const state = {
|
|
1564
|
+
controllerId,
|
|
1565
|
+
channel,
|
|
1566
|
+
waveforms,
|
|
1567
|
+
currentIndex: 0,
|
|
1568
|
+
interval,
|
|
1569
|
+
batchSize,
|
|
1570
|
+
timerId: null,
|
|
1571
|
+
active: true
|
|
1572
|
+
};
|
|
1573
|
+
state.timerId = setInterval(() => {
|
|
1574
|
+
if (!state.active) {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const batch = [];
|
|
1578
|
+
for (let i = 0; i < state.batchSize; i++) {
|
|
1579
|
+
batch.push(state.waveforms[state.currentIndex]);
|
|
1580
|
+
state.currentIndex = (state.currentIndex + 1) % state.waveforms.length;
|
|
1581
|
+
}
|
|
1582
|
+
const success = this.sendWaveform(controllerId, channel, batch);
|
|
1583
|
+
if (!success) {
|
|
1584
|
+
console.log(`[WS \u670D\u52A1\u5668] \u6301\u7EED\u64AD\u653E\u53D1\u9001\u5931\u8D25\uFF0C\u505C\u6B62\u64AD\u653E: ${key}`);
|
|
1585
|
+
this.stopContinuousPlayback(controllerId, channel);
|
|
1586
|
+
}
|
|
1587
|
+
}, interval);
|
|
1588
|
+
this.continuousPlaybacks.set(key, state);
|
|
1589
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u542F\u52A8\u6301\u7EED\u64AD\u653E: ${key}\uFF0C\u6CE2\u5F62\u6570: ${waveforms.length}\uFF0C\u95F4\u9694: ${interval}ms`);
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* 停止持续播放
|
|
1594
|
+
*
|
|
1595
|
+
* 停止指定通道的持续播放并清空波形队列。
|
|
1596
|
+
*
|
|
1597
|
+
* @param controllerId - 控制器 ID
|
|
1598
|
+
* @param channel - 目标通道 A 或 B
|
|
1599
|
+
* @returns 是否成功停止
|
|
1600
|
+
*/
|
|
1601
|
+
stopContinuousPlayback(controllerId, channel) {
|
|
1602
|
+
const key = `${controllerId}-${channel}`;
|
|
1603
|
+
const state = this.continuousPlaybacks.get(key);
|
|
1604
|
+
if (!state) {
|
|
1605
|
+
console.log(`[WS \u670D\u52A1\u5668] \u505C\u6B62\u6301\u7EED\u64AD\u653E\u5931\u8D25: ${key} \u4E0D\u5B58\u5728`);
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
state.active = false;
|
|
1609
|
+
if (state.timerId) {
|
|
1610
|
+
clearInterval(state.timerId);
|
|
1611
|
+
state.timerId = null;
|
|
1612
|
+
}
|
|
1613
|
+
this.clearWaveform(controllerId, channel);
|
|
1614
|
+
this.continuousPlaybacks.delete(key);
|
|
1615
|
+
console.log(`[WS \u670D\u52A1\u5668] \u5DF2\u505C\u6B62\u6301\u7EED\u64AD\u653E: ${key}`);
|
|
1616
|
+
return true;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* 检查是否正在持续播放
|
|
1620
|
+
*
|
|
1621
|
+
* @param controllerId - 控制器 ID
|
|
1622
|
+
* @param channel - 目标通道 A 或 B
|
|
1623
|
+
* @returns 是否正在持续播放
|
|
1624
|
+
*/
|
|
1625
|
+
isContinuousPlaying(controllerId, channel) {
|
|
1626
|
+
const key = `${controllerId}-${channel}`;
|
|
1627
|
+
const state = this.continuousPlaybacks.get(key);
|
|
1628
|
+
return state?.active ?? false;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* 获取持续播放状态
|
|
1632
|
+
*
|
|
1633
|
+
* @param controllerId - 控制器 ID
|
|
1634
|
+
* @param channel - 目标通道 A 或 B
|
|
1635
|
+
* @returns 持续播放状态或 null
|
|
1636
|
+
*/
|
|
1637
|
+
getContinuousPlaybackState(controllerId, channel) {
|
|
1638
|
+
const key = `${controllerId}-${channel}`;
|
|
1639
|
+
const state = this.continuousPlaybacks.get(key);
|
|
1640
|
+
if (!state) return null;
|
|
1641
|
+
return {
|
|
1642
|
+
waveformCount: state.waveforms.length,
|
|
1643
|
+
interval: state.interval,
|
|
1644
|
+
batchSize: state.batchSize,
|
|
1645
|
+
active: state.active
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
/** 获取 APP 扫描的二维码 URL */
|
|
1649
|
+
getQRCodeUrl(controllerId, host) {
|
|
1650
|
+
const wsUrl = `ws://${host}:${this.attachedPort}/${controllerId}`;
|
|
1651
|
+
return `https://www.dungeon-lab.com/app-download.php#DGLAB-SOCKET#${wsUrl}`;
|
|
1652
|
+
}
|
|
1653
|
+
/** 获取 APP 连接的 WebSocket URL */
|
|
1654
|
+
getWSUrl(controllerId, host) {
|
|
1655
|
+
return `ws://${host}:${this.attachedPort}/${controllerId}`;
|
|
1656
|
+
}
|
|
1657
|
+
/** 获取服务器端口 */
|
|
1658
|
+
getPort() {
|
|
1659
|
+
return this.attachedPort;
|
|
1660
|
+
}
|
|
1661
|
+
/** 获取客户端数量 */
|
|
1662
|
+
getClientCount() {
|
|
1663
|
+
return this.clients.size;
|
|
1664
|
+
}
|
|
1665
|
+
/** 获取绑定关系数量 */
|
|
1666
|
+
getRelationCount() {
|
|
1667
|
+
return this.relations.size;
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
// src/tools/device-tools.ts
|
|
1672
|
+
function registerDeviceTools(toolManager, sessionManager, wsServer, publicIp) {
|
|
1673
|
+
const localIp = getLocalIP();
|
|
1674
|
+
const ipAddress = publicIp || localIp;
|
|
1675
|
+
console.log(`[\u8BBE\u5907\u5DE5\u5177] PUBLIC_IP \u914D\u7F6E: "${publicIp || "(\u672A\u8BBE\u7F6E)"}"`);
|
|
1676
|
+
console.log(`[\u8BBE\u5907\u5DE5\u5177] \u672C\u5730 IP: ${localIp}`);
|
|
1677
|
+
console.log(`[\u8BBE\u5907\u5DE5\u5177] \u4F7F\u7528 IP: ${ipAddress}`);
|
|
1678
|
+
toolManager.registerTool(
|
|
1679
|
+
"dg_connect",
|
|
1680
|
+
`\u3010\u7B2C\u4E00\u6B65\u3011\u521B\u5EFADG-LAB\u8BBE\u5907\u8FDE\u63A5\u3002\u8FD4\u56DEdeviceId\uFF08\u540E\u7EED\u64CD\u4F5C\u5FC5\u9700\uFF09\u548CqrCodeUrl\uFF08\u4E8C\u7EF4\u7801\u94FE\u63A5\uFF09\u3002
|
|
1681
|
+
\u4F7F\u7528\u6D41\u7A0B\uFF1A1.\u8C03\u7528\u6B64\u5DE5\u5177\u83B7\u53D6\u4E8C\u7EF4\u7801 \u2192 2.\u751F\u6210\u4E8C\u7EF4\u7801\u540E\u8BA9\u7528\u6237\u7528DG-LAB APP\u626B\u7801 \u2192 3.\u7528\u6237\u8BF4\u626B\u4E86\u7801\u540E\u7528dg_get_status\u68C0\u67E5boundToApp\u662F\u5426\u4E3Atrue \u2192 4.boundToApp\u4E3Atrue\u540E\u624D\u80FD\u63A7\u5236\u8BBE\u5907\u3002
|
|
1682
|
+
\u6CE8\u610F\uFF1A\u6BCF\u6B21\u8C03\u7528\u4F1A\u521B\u5EFA\u65B0\u8FDE\u63A5\uFF0C\u5EFA\u8BAE\u5148\u7528dg_list_devices\u68C0\u67E5\u662F\u5426\u5DF2\u6709\u53EF\u7528\u8FDE\u63A5\u662F\u5C5E\u4E8E\u7528\u6237\u7684\u3002`,
|
|
1683
|
+
{
|
|
1684
|
+
type: "object",
|
|
1685
|
+
properties: {},
|
|
1686
|
+
required: []
|
|
1687
|
+
},
|
|
1688
|
+
async () => {
|
|
1689
|
+
try {
|
|
1690
|
+
const session = sessionManager.createSession();
|
|
1691
|
+
const clientId = wsServer.createController();
|
|
1692
|
+
sessionManager.updateConnectionState(session.deviceId, {
|
|
1693
|
+
clientId,
|
|
1694
|
+
connected: true
|
|
1695
|
+
});
|
|
1696
|
+
const qrCodeUrl = wsServer.getQRCodeUrl(clientId, ipAddress);
|
|
1697
|
+
return createToolResult(
|
|
1698
|
+
JSON.stringify({
|
|
1699
|
+
deviceId: session.deviceId,
|
|
1700
|
+
qrCodeUrl,
|
|
1701
|
+
message: "\u8BF7\u4F7F\u7528DG-LAB APP\u626B\u63CF\u4E8C\u7EF4\u7801\u8FDB\u884C\u7ED1\u5B9A"
|
|
1702
|
+
})
|
|
1703
|
+
);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
const error = err instanceof ConnectionError ? err : new ConnectionError(
|
|
1706
|
+
err instanceof Error ? err.message : "\u8FDE\u63A5\u5931\u8D25",
|
|
1707
|
+
{ code: "CONN_DEVICE_NOT_FOUND" /* CONN_DEVICE_NOT_FOUND */, cause: err instanceof Error ? err : void 0 }
|
|
1708
|
+
);
|
|
1709
|
+
return createToolError(`\u8FDE\u63A5\u5931\u8D25: ${error.message}`);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
);
|
|
1713
|
+
toolManager.registerTool(
|
|
1714
|
+
"dg_list_devices",
|
|
1715
|
+
`\u5217\u51FA\u6240\u6709\u5DF2\u521B\u5EFA\u7684\u8BBE\u5907\u8FDE\u63A5\u53CA\u5176\u72B6\u6001\u3002
|
|
1716
|
+
\u8FD4\u56DE\u5B57\u6BB5\u8BF4\u660E\uFF1A
|
|
1717
|
+
- deviceId: \u8BBE\u5907\u552F\u4E00\u6807\u8BC6\uFF0C\u7528\u4E8E\u540E\u7EED\u6240\u6709\u64CD\u4F5C
|
|
1718
|
+
- alias: \u8BBE\u5907\u522B\u540D\uFF08\u53EF\u9009\uFF0C\u7528\u4E8E\u65B9\u4FBF\u8BC6\u522B\uFF09
|
|
1719
|
+
- connected: \u4F1A\u8BDD\u662F\u5426\u5DF2\u5EFA\u7ACB
|
|
1720
|
+
- boundToApp: APP\u662F\u5426\u5DF2\u626B\u7801\u7ED1\u5B9A\uFF08\u5FC5\u987B\u4E3Atrue\u624D\u80FD\u63A7\u5236\u8BBE\u5907\uFF09
|
|
1721
|
+
- strengthA/B: \u5F53\u524DA/B\u901A\u9053\u5F3A\u5EA6(0-200)
|
|
1722
|
+
- strengthLimitA/B: A/B\u901A\u9053\u5F3A\u5EA6\u4E0A\u9650\uFF08\u7531APP\u8BBE\u7F6E\uFF09
|
|
1723
|
+
\u53EF\u9009\u53C2\u6570alias\u7528\u4E8E\u6309\u522B\u540D\u8FC7\u6EE4\u8BBE\u5907\u3002`,
|
|
1724
|
+
{
|
|
1725
|
+
type: "object",
|
|
1726
|
+
properties: {
|
|
1727
|
+
alias: {
|
|
1728
|
+
type: "string",
|
|
1729
|
+
description: "\u53EF\u9009\uFF0C\u6309\u522B\u540D\u8FC7\u6EE4\u8BBE\u5907\uFF08\u5927\u5C0F\u5199\u4E0D\u654F\u611F\uFF09"
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
required: []
|
|
1733
|
+
},
|
|
1734
|
+
async (params) => {
|
|
1735
|
+
let sessions = sessionManager.listSessions();
|
|
1736
|
+
const alias = params.alias;
|
|
1737
|
+
if (alias) {
|
|
1738
|
+
sessions = sessionManager.findByAlias(alias);
|
|
1739
|
+
}
|
|
1740
|
+
const devices = sessions.map((s) => {
|
|
1741
|
+
const isBound = s.clientId ? wsServer.isControllerBound(s.clientId) : false;
|
|
1742
|
+
return {
|
|
1743
|
+
deviceId: s.deviceId,
|
|
1744
|
+
alias: s.alias,
|
|
1745
|
+
connected: s.connected,
|
|
1746
|
+
boundToApp: isBound,
|
|
1747
|
+
strengthA: s.strengthA,
|
|
1748
|
+
strengthB: s.strengthB,
|
|
1749
|
+
strengthLimitA: s.strengthLimitA,
|
|
1750
|
+
strengthLimitB: s.strengthLimitB
|
|
1751
|
+
};
|
|
1752
|
+
});
|
|
1753
|
+
return createToolResult(JSON.stringify({ devices, count: devices.length }));
|
|
1754
|
+
}
|
|
1755
|
+
);
|
|
1756
|
+
toolManager.registerTool(
|
|
1757
|
+
"dg_set_alias",
|
|
1758
|
+
`\u4E3A\u8BBE\u5907\u8BBE\u7F6E\u81EA\u5B9A\u4E49\u522B\u540D\uFF0C\u65B9\u4FBF\u540E\u7EED\u901A\u8FC7\u522B\u540D\u67E5\u627E\u548C\u7BA1\u7406\u8BBE\u5907\u3002
|
|
1759
|
+
\u522B\u540D\u53EF\u4EE5\u662F\u7528\u6237\u540D\u3001\u6635\u79F0\u6216\u4EFB\u4F55\u4FBF\u4E8E\u8BC6\u522B\u7684\u540D\u79F0\u3002
|
|
1760
|
+
\u8BBE\u7F6E\u540E\u53EF\u901A\u8FC7dg_find_device\u6309\u522B\u540D\u67E5\u627E\uFF0C\u6216\u5728dg_disconnect\u4E2D\u4F7F\u7528\u522B\u540D\u65AD\u5F00\u8FDE\u63A5\u3002`,
|
|
1761
|
+
{
|
|
1762
|
+
type: "object",
|
|
1763
|
+
properties: {
|
|
1764
|
+
deviceId: {
|
|
1765
|
+
type: "string",
|
|
1766
|
+
description: "\u8BBE\u5907ID\uFF08\u4ECEdg_connect\u6216dg_list_devices\u83B7\u53D6\uFF09"
|
|
1767
|
+
},
|
|
1768
|
+
alias: {
|
|
1769
|
+
type: "string",
|
|
1770
|
+
description: "\u81EA\u5B9A\u4E49\u522B\u540D\uFF08\u5982\u7528\u6237\u540D\u3001\u6635\u79F0\u7B49\uFF0C\u652F\u6301\u4E2D\u6587\uFF09"
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
required: ["deviceId", "alias"]
|
|
1774
|
+
},
|
|
1775
|
+
async (params) => {
|
|
1776
|
+
const deviceId = params.deviceId;
|
|
1777
|
+
const alias = params.alias;
|
|
1778
|
+
if (!deviceId) {
|
|
1779
|
+
return createToolError("\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: deviceId");
|
|
1780
|
+
}
|
|
1781
|
+
if (!alias) {
|
|
1782
|
+
return createToolError("\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: alias");
|
|
1783
|
+
}
|
|
1784
|
+
const success = sessionManager.setAlias(deviceId, alias);
|
|
1785
|
+
if (!success) {
|
|
1786
|
+
return createToolError(`\u8BBE\u5907\u4E0D\u5B58\u5728: ${deviceId}`);
|
|
1787
|
+
}
|
|
1788
|
+
return createToolResult(
|
|
1789
|
+
JSON.stringify({
|
|
1790
|
+
success: true,
|
|
1791
|
+
deviceId,
|
|
1792
|
+
alias
|
|
1793
|
+
})
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
);
|
|
1797
|
+
toolManager.registerTool(
|
|
1798
|
+
"dg_find_device",
|
|
1799
|
+
`\u901A\u8FC7\u522B\u540D\u67E5\u627E\u8BBE\u5907\uFF08\u5927\u5C0F\u5199\u4E0D\u654F\u611F\uFF0C\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF09\u3002
|
|
1800
|
+
\u8FD4\u56DE\u6240\u6709\u5339\u914D\u7684\u8BBE\u5907\u5217\u8868\uFF0C\u5305\u542B\u5B8C\u6574\u72B6\u6001\u4FE1\u606F\u3002
|
|
1801
|
+
\u9002\u7528\u573A\u666F\uFF1A\u5F53\u77E5\u9053\u7528\u6237\u522B\u540D\u4F46\u4E0D\u8BB0\u5F97deviceId\u65F6\u4F7F\u7528\u3002
|
|
1802
|
+
\u8FD4\u56DE\u5B57\u6BB5\u4E0Edg_list_devices\u76F8\u540C\u3002`,
|
|
1803
|
+
{
|
|
1804
|
+
type: "object",
|
|
1805
|
+
properties: {
|
|
1806
|
+
alias: {
|
|
1807
|
+
type: "string",
|
|
1808
|
+
description: "\u8981\u67E5\u627E\u7684\u522B\u540D"
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
required: ["alias"]
|
|
1812
|
+
},
|
|
1813
|
+
async (params) => {
|
|
1814
|
+
const alias = params.alias;
|
|
1815
|
+
if (!alias) {
|
|
1816
|
+
return createToolError("\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: alias");
|
|
1817
|
+
}
|
|
1818
|
+
const sessions = sessionManager.findByAlias(alias);
|
|
1819
|
+
const devices = sessions.map((s) => {
|
|
1820
|
+
const isBound = s.clientId ? wsServer.isControllerBound(s.clientId) : false;
|
|
1821
|
+
return {
|
|
1822
|
+
deviceId: s.deviceId,
|
|
1823
|
+
alias: s.alias,
|
|
1824
|
+
connected: s.connected,
|
|
1825
|
+
boundToApp: isBound,
|
|
1826
|
+
strengthA: s.strengthA,
|
|
1827
|
+
strengthB: s.strengthB,
|
|
1828
|
+
strengthLimitA: s.strengthLimitA,
|
|
1829
|
+
strengthLimitB: s.strengthLimitB
|
|
1830
|
+
};
|
|
1831
|
+
});
|
|
1832
|
+
return createToolResult(
|
|
1833
|
+
JSON.stringify({
|
|
1834
|
+
devices,
|
|
1835
|
+
count: devices.length
|
|
1836
|
+
})
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
);
|
|
1840
|
+
toolManager.registerTool(
|
|
1841
|
+
"dg_disconnect",
|
|
1842
|
+
`\u65AD\u5F00\u5E76\u5220\u9664\u8BBE\u5907\u8FDE\u63A5\uFF0C\u91CA\u653E\u8D44\u6E90\u3002
|
|
1843
|
+
\u53EF\u901A\u8FC7deviceId\u7CBE\u786E\u5220\u9664\u5355\u4E2A\u8BBE\u5907\uFF0C\u6216\u901A\u8FC7alias\u5220\u9664\u6240\u6709\u5339\u914D\u7684\u8BBE\u5907\u3002
|
|
1844
|
+
\u6CE8\u610F\uFF1AdeviceId\u548Calias\u53EA\u80FD\u4E8C\u9009\u4E00\uFF0C\u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B\u3002
|
|
1845
|
+
\u5220\u9664\u540E\u8BBE\u5907\u9700\u8981\u91CD\u65B0\u8C03\u7528dg_connect\u521B\u5EFA\u65B0\u8FDE\u63A5\u3002`,
|
|
1846
|
+
{
|
|
1847
|
+
type: "object",
|
|
1848
|
+
properties: {
|
|
1849
|
+
deviceId: {
|
|
1850
|
+
type: "string",
|
|
1851
|
+
description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF09"
|
|
1852
|
+
},
|
|
1853
|
+
alias: {
|
|
1854
|
+
type: "string",
|
|
1855
|
+
description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09"
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
required: []
|
|
1859
|
+
},
|
|
1860
|
+
async (params) => {
|
|
1861
|
+
const deviceId = params.deviceId;
|
|
1862
|
+
const alias = params.alias;
|
|
1863
|
+
if (!deviceId && !alias) {
|
|
1864
|
+
return createToolError("\u5FC5\u987B\u63D0\u4F9B deviceId \u6216 alias \u53C2\u6570\u4E4B\u4E00");
|
|
1865
|
+
}
|
|
1866
|
+
if (deviceId && alias) {
|
|
1867
|
+
return createToolError("\u53EA\u80FD\u63D0\u4F9B deviceId \u6216 alias \u53C2\u6570\u4E4B\u4E00\uFF0C\u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B");
|
|
1868
|
+
}
|
|
1869
|
+
let sessionsToDelete = [];
|
|
1870
|
+
if (deviceId) {
|
|
1871
|
+
const session = sessionManager.getSession(deviceId);
|
|
1872
|
+
if (!session) {
|
|
1873
|
+
return createToolError(`\u8BBE\u5907\u4E0D\u5B58\u5728: ${deviceId}`);
|
|
1874
|
+
}
|
|
1875
|
+
sessionsToDelete.push(deviceId);
|
|
1876
|
+
} else if (alias) {
|
|
1877
|
+
const sessions = sessionManager.findByAlias(alias);
|
|
1878
|
+
if (sessions.length === 0) {
|
|
1879
|
+
return createToolError(`\u672A\u627E\u5230\u522B\u540D\u4E3A "${alias}" \u7684\u8BBE\u5907`);
|
|
1880
|
+
}
|
|
1881
|
+
sessionsToDelete = sessions.map((s) => s.deviceId);
|
|
1882
|
+
}
|
|
1883
|
+
const deletedDevices = [];
|
|
1884
|
+
for (const id of sessionsToDelete) {
|
|
1885
|
+
const session = sessionManager.getSession(id);
|
|
1886
|
+
if (session) {
|
|
1887
|
+
if (session.clientId) {
|
|
1888
|
+
wsServer.disconnectController(session.clientId);
|
|
1889
|
+
}
|
|
1890
|
+
deletedDevices.push({
|
|
1891
|
+
deviceId: session.deviceId,
|
|
1892
|
+
alias: session.alias
|
|
1893
|
+
});
|
|
1894
|
+
sessionManager.deleteSession(id);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return createToolResult(
|
|
1898
|
+
JSON.stringify({
|
|
1899
|
+
success: true,
|
|
1900
|
+
deletedCount: deletedDevices.length,
|
|
1901
|
+
deletedDevices
|
|
1902
|
+
})
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/waveform-storage.ts
|
|
1909
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
1910
|
+
import { dirname } from "path";
|
|
1911
|
+
var WaveformStorage = class {
|
|
1912
|
+
waveforms = /* @__PURE__ */ new Map();
|
|
1913
|
+
/**
|
|
1914
|
+
* 保存波形(如果名称已存在则覆盖)
|
|
1915
|
+
* @param waveform - 波形数据
|
|
1916
|
+
*/
|
|
1917
|
+
save(waveform) {
|
|
1918
|
+
this.waveforms.set(waveform.name, waveform);
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* 根据名称获取波形
|
|
1922
|
+
* @param name - 波形名称
|
|
1923
|
+
* @returns 波形数据或 null
|
|
1924
|
+
*/
|
|
1925
|
+
get(name) {
|
|
1926
|
+
return this.waveforms.get(name) || null;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* 列出所有波形
|
|
1930
|
+
* @returns 波形数组
|
|
1931
|
+
*/
|
|
1932
|
+
list() {
|
|
1933
|
+
return Array.from(this.waveforms.values());
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* 根据名称删除波形
|
|
1937
|
+
* @param name - 波形名称
|
|
1938
|
+
* @returns 是否成功删除
|
|
1939
|
+
*/
|
|
1940
|
+
delete(name) {
|
|
1941
|
+
return this.waveforms.delete(name);
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* 获取波形数量
|
|
1945
|
+
*/
|
|
1946
|
+
get count() {
|
|
1947
|
+
return this.waveforms.size;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* 检查波形是否存在
|
|
1951
|
+
* @param name - 波形名称
|
|
1952
|
+
* @returns 是否存在
|
|
1953
|
+
*/
|
|
1954
|
+
has(name) {
|
|
1955
|
+
return this.waveforms.has(name);
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* 清除所有波形
|
|
1959
|
+
*/
|
|
1960
|
+
clear() {
|
|
1961
|
+
this.waveforms.clear();
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* 转换为存储数据格式
|
|
1965
|
+
* @returns 存储数据
|
|
1966
|
+
*/
|
|
1967
|
+
toStorageData() {
|
|
1968
|
+
const waveforms = [];
|
|
1969
|
+
for (const waveform of this.waveforms.values()) {
|
|
1970
|
+
waveforms.push({
|
|
1971
|
+
name: waveform.name,
|
|
1972
|
+
metadata: waveform.metadata,
|
|
1973
|
+
sections: waveform.sections,
|
|
1974
|
+
rawData: waveform.rawData,
|
|
1975
|
+
hexWaveforms: waveform.hexWaveforms,
|
|
1976
|
+
createdAt: waveform.createdAt.toISOString()
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
return { version: 1, waveforms };
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* 从存储数据格式加载
|
|
1983
|
+
* @param data - 存储数据
|
|
1984
|
+
*/
|
|
1985
|
+
fromStorageData(data) {
|
|
1986
|
+
this.waveforms.clear();
|
|
1987
|
+
for (const stored of data.waveforms) {
|
|
1988
|
+
const waveform = {
|
|
1989
|
+
name: stored.name,
|
|
1990
|
+
metadata: stored.metadata,
|
|
1991
|
+
sections: stored.sections,
|
|
1992
|
+
rawData: stored.rawData,
|
|
1993
|
+
hexWaveforms: stored.hexWaveforms,
|
|
1994
|
+
createdAt: new Date(stored.createdAt)
|
|
1995
|
+
};
|
|
1996
|
+
this.waveforms.set(waveform.name, waveform);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
function persistWaveforms(storage, filePath = "./data/waveforms.json") {
|
|
2001
|
+
const data = storage.toStorageData();
|
|
2002
|
+
const json = JSON.stringify(data, null, 2);
|
|
2003
|
+
const dir = dirname(filePath);
|
|
2004
|
+
if (!existsSync(dir)) {
|
|
2005
|
+
mkdirSync(dir, { recursive: true });
|
|
2006
|
+
}
|
|
2007
|
+
writeFileSync(filePath, json, "utf8");
|
|
2008
|
+
}
|
|
2009
|
+
function loadWaveforms(storage, filePath = "./data/waveforms.json") {
|
|
2010
|
+
if (!existsSync(filePath)) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
try {
|
|
2014
|
+
const json = readFileSync(filePath, "utf8");
|
|
2015
|
+
const data = JSON.parse(json);
|
|
2016
|
+
if (data.version !== 1) {
|
|
2017
|
+
console.warn(`\u672A\u77E5\u7684\u6CE2\u5F62\u5B58\u50A8\u7248\u672C: ${data.version}`);
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
storage.fromStorageData(data);
|
|
2021
|
+
return true;
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
console.error("\u52A0\u8F7D\u6CE2\u5F62\u5931\u8D25:", error);
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/waveform-parser.ts
|
|
2029
|
+
var FREQUENCY_DATASET = [
|
|
2030
|
+
// (10..50) step 1 → 索引 0-40
|
|
2031
|
+
10,
|
|
2032
|
+
11,
|
|
2033
|
+
12,
|
|
2034
|
+
13,
|
|
2035
|
+
14,
|
|
2036
|
+
15,
|
|
2037
|
+
16,
|
|
2038
|
+
17,
|
|
2039
|
+
18,
|
|
2040
|
+
19,
|
|
2041
|
+
20,
|
|
2042
|
+
21,
|
|
2043
|
+
22,
|
|
2044
|
+
23,
|
|
2045
|
+
24,
|
|
2046
|
+
25,
|
|
2047
|
+
26,
|
|
2048
|
+
27,
|
|
2049
|
+
28,
|
|
2050
|
+
29,
|
|
2051
|
+
30,
|
|
2052
|
+
31,
|
|
2053
|
+
32,
|
|
2054
|
+
33,
|
|
2055
|
+
34,
|
|
2056
|
+
35,
|
|
2057
|
+
36,
|
|
2058
|
+
37,
|
|
2059
|
+
38,
|
|
2060
|
+
39,
|
|
2061
|
+
40,
|
|
2062
|
+
41,
|
|
2063
|
+
42,
|
|
2064
|
+
43,
|
|
2065
|
+
44,
|
|
2066
|
+
45,
|
|
2067
|
+
46,
|
|
2068
|
+
47,
|
|
2069
|
+
48,
|
|
2070
|
+
49,
|
|
2071
|
+
50,
|
|
2072
|
+
// (52..80) step 2 → 索引 41-55
|
|
2073
|
+
52,
|
|
2074
|
+
54,
|
|
2075
|
+
56,
|
|
2076
|
+
58,
|
|
2077
|
+
60,
|
|
2078
|
+
62,
|
|
2079
|
+
64,
|
|
2080
|
+
66,
|
|
2081
|
+
68,
|
|
2082
|
+
70,
|
|
2083
|
+
72,
|
|
2084
|
+
74,
|
|
2085
|
+
76,
|
|
2086
|
+
78,
|
|
2087
|
+
80,
|
|
2088
|
+
// (85..100) step 5 → 索引 56-59
|
|
2089
|
+
85,
|
|
2090
|
+
90,
|
|
2091
|
+
95,
|
|
2092
|
+
100,
|
|
2093
|
+
// (110..200) step 10 → 索引 60-69
|
|
2094
|
+
110,
|
|
2095
|
+
120,
|
|
2096
|
+
130,
|
|
2097
|
+
140,
|
|
2098
|
+
150,
|
|
2099
|
+
160,
|
|
2100
|
+
170,
|
|
2101
|
+
180,
|
|
2102
|
+
190,
|
|
2103
|
+
200,
|
|
2104
|
+
// (233..400) step 33 → 索引 70-75
|
|
2105
|
+
233,
|
|
2106
|
+
266,
|
|
2107
|
+
300,
|
|
2108
|
+
333,
|
|
2109
|
+
366,
|
|
2110
|
+
400,
|
|
2111
|
+
// (450..600) step 50 → 索引 76-79
|
|
2112
|
+
450,
|
|
2113
|
+
500,
|
|
2114
|
+
550,
|
|
2115
|
+
600,
|
|
2116
|
+
// (700..1000) step 100 → 索引 80-83
|
|
2117
|
+
700,
|
|
2118
|
+
800,
|
|
2119
|
+
900,
|
|
2120
|
+
1e3
|
|
2121
|
+
];
|
|
2122
|
+
var DURATION_DATASET = Array.from({ length: 100 }, (_, i) => i + 1);
|
|
2123
|
+
function getFrequencyFromIndex(index) {
|
|
2124
|
+
const clampedIndex = Math.max(0, Math.min(83, Math.floor(index)));
|
|
2125
|
+
return FREQUENCY_DATASET[clampedIndex] ?? 10;
|
|
2126
|
+
}
|
|
2127
|
+
function getDurationFromIndex(index) {
|
|
2128
|
+
const clampedIndex = Math.max(0, Math.min(99, Math.floor(index)));
|
|
2129
|
+
return DURATION_DATASET[clampedIndex] ?? 1;
|
|
2130
|
+
}
|
|
2131
|
+
function getOutputValue(x) {
|
|
2132
|
+
let output;
|
|
2133
|
+
if (x >= 10 && x <= 100) {
|
|
2134
|
+
output = x;
|
|
2135
|
+
} else if (x > 100 && x <= 600) {
|
|
2136
|
+
output = (x - 100) / 5 + 100;
|
|
2137
|
+
} else if (x > 600 && x <= 1e3) {
|
|
2138
|
+
output = (x - 600) / 10 + 200;
|
|
2139
|
+
} else if (x < 10) {
|
|
2140
|
+
output = 10;
|
|
2141
|
+
} else {
|
|
2142
|
+
output = 240;
|
|
2143
|
+
}
|
|
2144
|
+
return Math.max(10, Math.min(240, Math.round(output)));
|
|
2145
|
+
}
|
|
2146
|
+
function parseWaveform(data, name) {
|
|
2147
|
+
if (!data.startsWith("Dungeonlab+pulse:")) {
|
|
2148
|
+
throw new WaveformError("\u65E0\u6548\u7684\u6CE2\u5F62\u683C\u5F0F: \u5FC5\u987B\u4EE5 'Dungeonlab+pulse:' \u5F00\u5934", {
|
|
2149
|
+
code: "WAVEFORM_INVALID_FORMAT" /* WAVEFORM_INVALID_FORMAT */,
|
|
2150
|
+
context: { name, prefix: data.substring(0, 20) }
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
const cleanData = data.replace(/^Dungeonlab\+pulse:/i, "");
|
|
2154
|
+
const sectionParts = cleanData.split("+section+");
|
|
2155
|
+
if (sectionParts.length === 0 || !sectionParts[0]) {
|
|
2156
|
+
throw new WaveformError("\u65E0\u6548\u7684\u6CE2\u5F62\u6570\u636E: \u672A\u627E\u5230\u5C0F\u8282", {
|
|
2157
|
+
code: "WAVEFORM_PARSE_FAILED" /* WAVEFORM_PARSE_FAILED */,
|
|
2158
|
+
context: { name }
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
const firstPart = sectionParts[0];
|
|
2162
|
+
const equalIdx = firstPart.indexOf("=");
|
|
2163
|
+
if (equalIdx === -1) {
|
|
2164
|
+
throw new WaveformError("\u65E0\u6548\u7684\u6CE2\u5F62\u683C\u5F0F: \u7F3A\u5C11\u5168\u5C40\u8BBE\u7F6E\u7684 '=' \u5206\u9694\u7B26", {
|
|
2165
|
+
code: "WAVEFORM_INVALID_FORMAT" /* WAVEFORM_INVALID_FORMAT */,
|
|
2166
|
+
context: { name }
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
const settingsPart = firstPart.substring(0, equalIdx);
|
|
2170
|
+
const settingsValues = settingsPart.split(",");
|
|
2171
|
+
const globalSettings = {
|
|
2172
|
+
sectionRestTime: Number(settingsValues[0]) || 0,
|
|
2173
|
+
playbackSpeed: Number(settingsValues[1]) || 1,
|
|
2174
|
+
frequencyBalance: Number(settingsValues[2]) || 8
|
|
2175
|
+
};
|
|
2176
|
+
const sections = [];
|
|
2177
|
+
const startFrequencyIndices = [];
|
|
2178
|
+
const endFrequencyIndices = [];
|
|
2179
|
+
const durationIndices = [];
|
|
2180
|
+
const frequencyModes = [];
|
|
2181
|
+
const sectionEnabled = [];
|
|
2182
|
+
const firstSectionData = firstPart.substring(equalIdx + 1);
|
|
2183
|
+
const allSectionData = [firstSectionData, ...sectionParts.slice(1)];
|
|
2184
|
+
for (let i = 0; i < allSectionData.length && i < 10; i++) {
|
|
2185
|
+
const sectionData = allSectionData[i];
|
|
2186
|
+
if (!sectionData) continue;
|
|
2187
|
+
const slashIdx = sectionData.indexOf("/");
|
|
2188
|
+
if (slashIdx === -1) {
|
|
2189
|
+
throw new WaveformError(`\u65E0\u6548\u7684\u5C0F\u8282 ${i + 1}: \u7F3A\u5C11 '/' \u5206\u9694\u7B26`, {
|
|
2190
|
+
code: "WAVEFORM_INVALID_FORMAT" /* WAVEFORM_INVALID_FORMAT */,
|
|
2191
|
+
context: { name, sectionIndex: i + 1 }
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
const headerPart = sectionData.substring(0, slashIdx);
|
|
2195
|
+
const shapePart = sectionData.substring(slashIdx + 1);
|
|
2196
|
+
const headerValues = headerPart.split(",");
|
|
2197
|
+
const freqRange1Index = Number(headerValues[0]) || 0;
|
|
2198
|
+
const freqRange2Index = Number(headerValues[1]) || 0;
|
|
2199
|
+
const durationIndex = Number(headerValues[2]) || 0;
|
|
2200
|
+
const freqMode = Number(headerValues[3]) || 1;
|
|
2201
|
+
const enabled = headerValues[4] !== "0";
|
|
2202
|
+
startFrequencyIndices.push(freqRange1Index);
|
|
2203
|
+
endFrequencyIndices.push(freqRange2Index);
|
|
2204
|
+
durationIndices.push(durationIndex);
|
|
2205
|
+
frequencyModes.push(freqMode);
|
|
2206
|
+
sectionEnabled.push(enabled);
|
|
2207
|
+
const shapePoints = [];
|
|
2208
|
+
const shapeItems = shapePart.split(",");
|
|
2209
|
+
for (const item of shapeItems) {
|
|
2210
|
+
if (!item) continue;
|
|
2211
|
+
const [strengthStr, anchorStr] = item.split("-");
|
|
2212
|
+
const strength = Math.round(Number(strengthStr) || 0);
|
|
2213
|
+
const isAnchor = anchorStr === "1";
|
|
2214
|
+
shapePoints.push({
|
|
2215
|
+
strength: Math.max(0, Math.min(100, strength)),
|
|
2216
|
+
isAnchor,
|
|
2217
|
+
shapeType: isAnchor ? 1 : 0
|
|
2218
|
+
// 兼容性
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
if (shapePoints.length < 2) {
|
|
2222
|
+
throw new WaveformError(`\u65E0\u6548\u7684\u5C0F\u8282 ${i + 1}: \u5FC5\u987B\u81F3\u5C11\u6709 2 \u4E2A\u5F62\u72B6\u70B9`, {
|
|
2223
|
+
code: "WAVEFORM_INVALID_FORMAT" /* WAVEFORM_INVALID_FORMAT */,
|
|
2224
|
+
context: { name, sectionIndex: i + 1, shapePointCount: shapePoints.length }
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
const startFreq = getFrequencyFromIndex(freqRange1Index);
|
|
2228
|
+
const endFreq = getFrequencyFromIndex(freqRange2Index);
|
|
2229
|
+
const duration = getDurationFromIndex(durationIndex);
|
|
2230
|
+
if (enabled) {
|
|
2231
|
+
sections.push({
|
|
2232
|
+
index: i,
|
|
2233
|
+
enabled: true,
|
|
2234
|
+
frequencyRange1Index: freqRange1Index,
|
|
2235
|
+
frequencyRange2Index: freqRange2Index,
|
|
2236
|
+
durationIndex,
|
|
2237
|
+
frequencyMode: freqMode,
|
|
2238
|
+
shape: shapePoints,
|
|
2239
|
+
startFrequency: startFreq,
|
|
2240
|
+
endFrequency: endFreq,
|
|
2241
|
+
duration
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (sections.length === 0) {
|
|
2246
|
+
throw new WaveformError("\u65E0\u6548\u7684\u6CE2\u5F62\u6570\u636E: \u6CA1\u6709\u542F\u7528\u7684\u5C0F\u8282", {
|
|
2247
|
+
code: "WAVEFORM_PARSE_FAILED" /* WAVEFORM_PARSE_FAILED */,
|
|
2248
|
+
context: { name }
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
const metadata = {
|
|
2252
|
+
globalSettings,
|
|
2253
|
+
startFrequencyIndices,
|
|
2254
|
+
endFrequencyIndices,
|
|
2255
|
+
durationIndices,
|
|
2256
|
+
frequencyModes,
|
|
2257
|
+
sectionEnabled,
|
|
2258
|
+
// 兼容性字段
|
|
2259
|
+
startFrequencies: [
|
|
2260
|
+
getFrequencyFromIndex(startFrequencyIndices[0] ?? 0),
|
|
2261
|
+
getFrequencyFromIndex(startFrequencyIndices[1] ?? 0),
|
|
2262
|
+
getFrequencyFromIndex(startFrequencyIndices[2] ?? 0)
|
|
2263
|
+
],
|
|
2264
|
+
endFrequencies: [
|
|
2265
|
+
getFrequencyFromIndex(endFrequencyIndices[0] ?? 0),
|
|
2266
|
+
getFrequencyFromIndex(endFrequencyIndices[1] ?? 0),
|
|
2267
|
+
getFrequencyFromIndex(endFrequencyIndices[2] ?? 0)
|
|
2268
|
+
],
|
|
2269
|
+
durations: [
|
|
2270
|
+
getDurationFromIndex(durationIndices[0] ?? 0),
|
|
2271
|
+
getDurationFromIndex(durationIndices[1] ?? 0),
|
|
2272
|
+
getDurationFromIndex(durationIndices[2] ?? 0)
|
|
2273
|
+
],
|
|
2274
|
+
frequencyModes_legacy: [
|
|
2275
|
+
frequencyModes[0] ?? 1,
|
|
2276
|
+
frequencyModes[1] ?? 1,
|
|
2277
|
+
frequencyModes[2] ?? 1
|
|
2278
|
+
],
|
|
2279
|
+
section2Enabled: sectionEnabled[1] ?? false,
|
|
2280
|
+
section3Enabled: sectionEnabled[2] ?? false,
|
|
2281
|
+
playbackSpeed: globalSettings.playbackSpeed
|
|
2282
|
+
};
|
|
2283
|
+
const hexWaveforms = convertToHexWaveforms(sections, globalSettings.playbackSpeed);
|
|
2284
|
+
return {
|
|
2285
|
+
name,
|
|
2286
|
+
metadata,
|
|
2287
|
+
sections,
|
|
2288
|
+
rawData: data,
|
|
2289
|
+
hexWaveforms,
|
|
2290
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
function convertToHexWaveforms(sections, _playbackSpeed = 1) {
|
|
2294
|
+
const hexWaveforms = [];
|
|
2295
|
+
for (const section of sections) {
|
|
2296
|
+
if (section.shape.length === 0) continue;
|
|
2297
|
+
const shapeCount = section.shape.length;
|
|
2298
|
+
const pulseElementDuration = shapeCount;
|
|
2299
|
+
const sectionDuration = section.duration;
|
|
2300
|
+
const startFreq = section.startFrequency;
|
|
2301
|
+
const endFreq = section.endFrequency;
|
|
2302
|
+
const freqMode = section.frequencyMode;
|
|
2303
|
+
const pulseElementCount = Math.max(1, Math.ceil(sectionDuration / pulseElementDuration));
|
|
2304
|
+
const actualDuration = pulseElementCount * pulseElementDuration;
|
|
2305
|
+
const waveformFreq = [];
|
|
2306
|
+
const waveformStrength = [];
|
|
2307
|
+
for (let elementIdx = 0; elementIdx < pulseElementCount; elementIdx++) {
|
|
2308
|
+
for (let shapeIdx = 0; shapeIdx < shapeCount; shapeIdx++) {
|
|
2309
|
+
const currentPoint = section.shape[shapeIdx];
|
|
2310
|
+
const strength = currentPoint?.strength ?? 0;
|
|
2311
|
+
const currentTime = elementIdx * pulseElementDuration + shapeIdx;
|
|
2312
|
+
const sectionProgress = currentTime / actualDuration;
|
|
2313
|
+
const elementProgress = shapeIdx / shapeCount;
|
|
2314
|
+
let freq;
|
|
2315
|
+
switch (freqMode) {
|
|
2316
|
+
case 1:
|
|
2317
|
+
freq = getOutputValue(startFreq);
|
|
2318
|
+
break;
|
|
2319
|
+
case 2:
|
|
2320
|
+
freq = getOutputValue(startFreq + (endFreq - startFreq) * sectionProgress);
|
|
2321
|
+
break;
|
|
2322
|
+
case 3:
|
|
2323
|
+
freq = getOutputValue(startFreq + (endFreq - startFreq) * elementProgress);
|
|
2324
|
+
break;
|
|
2325
|
+
case 4:
|
|
2326
|
+
{
|
|
2327
|
+
const elementProgress4 = pulseElementCount > 1 ? elementIdx / (pulseElementCount - 1) : 0;
|
|
2328
|
+
freq = getOutputValue(startFreq + (endFreq - startFreq) * elementProgress4);
|
|
2329
|
+
}
|
|
2330
|
+
break;
|
|
2331
|
+
default:
|
|
2332
|
+
freq = getOutputValue(startFreq);
|
|
2333
|
+
}
|
|
2334
|
+
for (let n = 0; n < 4; n++) {
|
|
2335
|
+
waveformStrength.push(Math.max(0, Math.min(100, Math.round(strength))));
|
|
2336
|
+
waveformFreq.push(Math.round(freq));
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
for (let i = 0; i < waveformFreq.length; i += 4) {
|
|
2341
|
+
const freqHex = [
|
|
2342
|
+
waveformFreq[i] ?? 10,
|
|
2343
|
+
waveformFreq[i + 1] ?? 10,
|
|
2344
|
+
waveformFreq[i + 2] ?? 10,
|
|
2345
|
+
waveformFreq[i + 3] ?? 10
|
|
2346
|
+
].map((v) => Math.max(10, Math.min(240, v)).toString(16).padStart(2, "0")).join("");
|
|
2347
|
+
const strengthHex = [
|
|
2348
|
+
waveformStrength[i] ?? 0,
|
|
2349
|
+
waveformStrength[i + 1] ?? 0,
|
|
2350
|
+
waveformStrength[i + 2] ?? 0,
|
|
2351
|
+
waveformStrength[i + 3] ?? 0
|
|
2352
|
+
].map((v) => Math.max(0, Math.min(100, v)).toString(16).padStart(2, "0")).join("");
|
|
2353
|
+
hexWaveforms.push(freqHex + strengthHex);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return hexWaveforms;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/tools/waveform-tools.ts
|
|
2360
|
+
var waveformStorage = null;
|
|
2361
|
+
var storagePath = "./data/waveforms.json";
|
|
2362
|
+
function initWaveformStorage(storage, path) {
|
|
2363
|
+
waveformStorage = storage || new WaveformStorage();
|
|
2364
|
+
if (path) storagePath = path;
|
|
2365
|
+
}
|
|
2366
|
+
function getWaveformStorage() {
|
|
2367
|
+
if (!waveformStorage) {
|
|
2368
|
+
waveformStorage = new WaveformStorage();
|
|
2369
|
+
}
|
|
2370
|
+
return waveformStorage;
|
|
2371
|
+
}
|
|
2372
|
+
function createToolError2(message) {
|
|
2373
|
+
return {
|
|
2374
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2375
|
+
isError: true
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function createToolSuccess(data) {
|
|
2379
|
+
return {
|
|
2380
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
var dgParseWaveformTool = {
|
|
2384
|
+
name: "dg_parse_waveform",
|
|
2385
|
+
description: `\u89E3\u6790\u5E76\u4FDD\u5B58\u6CE2\u5F62\u6570\u636E\u3002
|
|
2386
|
+
\u8F93\u5165\u683C\u5F0F\uFF1ADungeonlab+pulse:\u5F00\u5934\u7684Base64\u7F16\u7801\u5B57\u7B26\u4E32\uFF08\u4ECEDG-LAB APP\u5BFC\u51FA\uFF09\u3002
|
|
2387
|
+
\u89E3\u6790\u540E\u4FDD\u5B58\u4E3AhexWaveforms\u6570\u7EC4\uFF0C\u53EF\u901A\u8FC7dg_get_waveform\u83B7\u53D6\u5E76\u7528dg_send_waveform\u53D1\u9001\u3002
|
|
2388
|
+
\u5982\u679Cname\u5DF2\u5B58\u5728\u4F1A\u8986\u76D6\u539F\u6709\u6CE2\u5F62\u3002`,
|
|
2389
|
+
inputSchema: {
|
|
2390
|
+
type: "object",
|
|
2391
|
+
properties: {
|
|
2392
|
+
hexData: {
|
|
2393
|
+
type: "string",
|
|
2394
|
+
description: "\u6CE2\u5F62\u6570\u636E\uFF08Dungeonlab+pulse:\u683C\u5F0F\u6587\u672C\uFF09"
|
|
2395
|
+
},
|
|
2396
|
+
name: {
|
|
2397
|
+
type: "string",
|
|
2398
|
+
description: "\u6CE2\u5F62\u540D\u79F0\uFF0C\u7528\u4E8E\u4FDD\u5B58\u548C\u540E\u7EED\u5F15\u7528"
|
|
2399
|
+
}
|
|
2400
|
+
},
|
|
2401
|
+
required: ["hexData", "name"]
|
|
2402
|
+
},
|
|
2403
|
+
handler: async (params) => {
|
|
2404
|
+
const hexData = params.hexData;
|
|
2405
|
+
const name = params.name;
|
|
2406
|
+
if (!hexData || typeof hexData !== "string") {
|
|
2407
|
+
return createToolError2("hexData \u662F\u5FC5\u9700\u7684\u4E14\u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
2408
|
+
}
|
|
2409
|
+
if (!name || typeof name !== "string" || name.trim().length === 0) {
|
|
2410
|
+
return createToolError2("name \u662F\u5FC5\u9700\u7684\u4E14\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
2411
|
+
}
|
|
2412
|
+
try {
|
|
2413
|
+
const waveform = parseWaveform(hexData, name.trim());
|
|
2414
|
+
const storage = getWaveformStorage();
|
|
2415
|
+
const existed = storage.has(name.trim());
|
|
2416
|
+
storage.save(waveform);
|
|
2417
|
+
persistWaveforms(storage, storagePath);
|
|
2418
|
+
return createToolSuccess({
|
|
2419
|
+
success: true,
|
|
2420
|
+
name: waveform.name,
|
|
2421
|
+
overwritten: existed,
|
|
2422
|
+
hexWaveformCount: waveform.hexWaveforms.length
|
|
2423
|
+
});
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
if (error instanceof Error) {
|
|
2426
|
+
return createToolError2(error.message);
|
|
2427
|
+
}
|
|
2428
|
+
return createToolError2("\u89E3\u6790\u6CE2\u5F62\u6570\u636E\u5931\u8D25");
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
var dgListWaveformsTool = {
|
|
2433
|
+
name: "dg_list_waveforms",
|
|
2434
|
+
description: `\u5217\u51FA\u6240\u6709\u5DF2\u4FDD\u5B58\u7684\u6CE2\u5F62\u540D\u79F0\u548C\u6570\u636E\u91CF\u3002
|
|
2435
|
+
\u8FD4\u56DE\u6BCF\u4E2A\u6CE2\u5F62\u7684name\u548ChexWaveformCount\uFF08\u6CE2\u5F62\u6570\u636E\u6761\u6570\uFF09\u3002
|
|
2436
|
+
\u7528\u4E8E\u67E5\u770B\u53EF\u7528\u6CE2\u5F62\uFF0C\u7136\u540E\u901A\u8FC7dg_get_waveform\u83B7\u53D6\u5177\u4F53\u6570\u636E\u3002`,
|
|
2437
|
+
inputSchema: {
|
|
2438
|
+
type: "object",
|
|
2439
|
+
properties: {},
|
|
2440
|
+
required: []
|
|
2441
|
+
},
|
|
2442
|
+
handler: async () => {
|
|
2443
|
+
const storage = getWaveformStorage();
|
|
2444
|
+
const waveforms = storage.list();
|
|
2445
|
+
const list = waveforms.map((w) => ({
|
|
2446
|
+
name: w.name,
|
|
2447
|
+
hexWaveformCount: w.hexWaveforms.length
|
|
2448
|
+
}));
|
|
2449
|
+
return createToolSuccess({
|
|
2450
|
+
count: list.length,
|
|
2451
|
+
waveforms: list
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
var dgGetWaveformTool = {
|
|
2456
|
+
name: "dg_get_waveform",
|
|
2457
|
+
description: `\u6309\u540D\u79F0\u83B7\u53D6\u6CE2\u5F62\u7684hexWaveforms\u6570\u7EC4\u3002
|
|
2458
|
+
\u8FD4\u56DE\u7684hexWaveforms\u53EF\u76F4\u63A5\u4F20\u7ED9dg_send_waveform\u7684waveforms\u53C2\u6570\u4F7F\u7528\u3002
|
|
2459
|
+
\u5178\u578B\u6D41\u7A0B\uFF1Adg_list_waveforms\u67E5\u770B\u53EF\u7528\u6CE2\u5F62 \u2192 dg_get_waveform\u83B7\u53D6\u6570\u636E \u2192 dg_send_waveform\u53D1\u9001\u5230\u8BBE\u5907\u3002`,
|
|
2460
|
+
inputSchema: {
|
|
2461
|
+
type: "object",
|
|
2462
|
+
properties: {
|
|
2463
|
+
name: {
|
|
2464
|
+
type: "string",
|
|
2465
|
+
description: "\u6CE2\u5F62\u540D\u79F0"
|
|
2466
|
+
}
|
|
2467
|
+
},
|
|
2468
|
+
required: ["name"]
|
|
2469
|
+
},
|
|
2470
|
+
handler: async (params) => {
|
|
2471
|
+
const name = params.name;
|
|
2472
|
+
if (!name || typeof name !== "string") {
|
|
2473
|
+
return createToolError2("name \u662F\u5FC5\u9700\u7684\u4E14\u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
2474
|
+
}
|
|
2475
|
+
const storage = getWaveformStorage();
|
|
2476
|
+
const waveform = storage.get(name);
|
|
2477
|
+
if (!waveform) {
|
|
2478
|
+
return createToolError2(`\u6CE2\u5F62\u672A\u627E\u5230: ${name}`);
|
|
2479
|
+
}
|
|
2480
|
+
return createToolSuccess({
|
|
2481
|
+
name: waveform.name,
|
|
2482
|
+
hexWaveforms: waveform.hexWaveforms
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
var dgDeleteWaveformTool = {
|
|
2487
|
+
name: "dg_delete_waveform",
|
|
2488
|
+
description: `\u6309\u540D\u79F0\u5220\u9664\u5DF2\u4FDD\u5B58\u7684\u6CE2\u5F62\u3002
|
|
2489
|
+
\u5220\u9664\u540E\u65E0\u6CD5\u6062\u590D\uFF0C\u9700\u8981\u91CD\u65B0\u7528dg_parse_waveform\u89E3\u6790\u4FDD\u5B58\u3002`,
|
|
2490
|
+
inputSchema: {
|
|
2491
|
+
type: "object",
|
|
2492
|
+
properties: {
|
|
2493
|
+
name: {
|
|
2494
|
+
type: "string",
|
|
2495
|
+
description: "\u8981\u5220\u9664\u7684\u6CE2\u5F62\u540D\u79F0"
|
|
2496
|
+
}
|
|
2497
|
+
},
|
|
2498
|
+
required: ["name"]
|
|
2499
|
+
},
|
|
2500
|
+
handler: async (params) => {
|
|
2501
|
+
const name = params.name;
|
|
2502
|
+
if (!name || typeof name !== "string") {
|
|
2503
|
+
return createToolError2("name \u662F\u5FC5\u9700\u7684\u4E14\u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
2504
|
+
}
|
|
2505
|
+
const storage = getWaveformStorage();
|
|
2506
|
+
if (!storage.has(name)) {
|
|
2507
|
+
return createToolError2(`\u6CE2\u5F62\u672A\u627E\u5230: ${name}`);
|
|
2508
|
+
}
|
|
2509
|
+
storage.delete(name);
|
|
2510
|
+
persistWaveforms(storage, storagePath);
|
|
2511
|
+
return createToolSuccess({
|
|
2512
|
+
success: true,
|
|
2513
|
+
deleted: name
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
function getWaveformTools() {
|
|
2518
|
+
return [
|
|
2519
|
+
dgParseWaveformTool,
|
|
2520
|
+
dgListWaveformsTool,
|
|
2521
|
+
dgGetWaveformTool,
|
|
2522
|
+
dgDeleteWaveformTool
|
|
2523
|
+
];
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// src/tools/control-tools.ts
|
|
2527
|
+
function resolveDevice(sessionManager, deviceId, alias) {
|
|
2528
|
+
if (!deviceId && !alias) {
|
|
2529
|
+
return { error: "\u5FC5\u987B\u63D0\u4F9B deviceId \u6216 alias \u53C2\u6570\u4E4B\u4E00" };
|
|
2530
|
+
}
|
|
2531
|
+
if (deviceId) {
|
|
2532
|
+
const session = sessionManager.getSession(deviceId);
|
|
2533
|
+
if (!session) {
|
|
2534
|
+
return { error: `\u8BBE\u5907\u4E0D\u5B58\u5728: ${deviceId}` };
|
|
2535
|
+
}
|
|
2536
|
+
return { session };
|
|
2537
|
+
}
|
|
2538
|
+
const sessions = sessionManager.findByAlias(alias);
|
|
2539
|
+
if (sessions.length === 0) {
|
|
2540
|
+
return { error: `\u672A\u627E\u5230\u522B\u540D\u4E3A "${alias}" \u7684\u8BBE\u5907` };
|
|
2541
|
+
}
|
|
2542
|
+
if (sessions.length > 1) {
|
|
2543
|
+
return { error: `\u522B\u540D "${alias}" \u5339\u914D\u5230\u591A\u4E2A\u8BBE\u5907 (${sessions.length} \u4E2A)\uFF0C\u8BF7\u4F7F\u7528 deviceId \u6307\u5B9A` };
|
|
2544
|
+
}
|
|
2545
|
+
return { session: sessions[0] };
|
|
2546
|
+
}
|
|
2547
|
+
function validateChannel(channel) {
|
|
2548
|
+
if (!channel) {
|
|
2549
|
+
return { error: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: channel" };
|
|
2550
|
+
}
|
|
2551
|
+
if (channel !== "A" && channel !== "B") {
|
|
2552
|
+
return { error: `\u65E0\u6548\u7684\u901A\u9053: ${channel}\uFF0C\u5FC5\u987B\u662F "A" \u6216 "B"` };
|
|
2553
|
+
}
|
|
2554
|
+
return { channel };
|
|
2555
|
+
}
|
|
2556
|
+
function validateStrengthValue(value) {
|
|
2557
|
+
if (value === void 0 || value === null) {
|
|
2558
|
+
return { error: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: value" };
|
|
2559
|
+
}
|
|
2560
|
+
const num = Number(value);
|
|
2561
|
+
if (isNaN(num) || num < 0 || num > 200) {
|
|
2562
|
+
return { error: `\u65E0\u6548\u7684\u5F3A\u5EA6\u503C: ${value}\uFF0C\u5FC5\u987B\u5728 0-200 \u8303\u56F4\u5185` };
|
|
2563
|
+
}
|
|
2564
|
+
return { value: num };
|
|
2565
|
+
}
|
|
2566
|
+
function validateStrengthMode(mode) {
|
|
2567
|
+
if (!mode) {
|
|
2568
|
+
return { error: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: mode" };
|
|
2569
|
+
}
|
|
2570
|
+
if (mode !== "increase" && mode !== "decrease" && mode !== "set") {
|
|
2571
|
+
return { error: `\u65E0\u6548\u7684\u6A21\u5F0F: ${mode}\uFF0C\u5FC5\u987B\u662F "increase"\u3001"decrease" \u6216 "set"` };
|
|
2572
|
+
}
|
|
2573
|
+
return { mode };
|
|
2574
|
+
}
|
|
2575
|
+
function validateWaveforms(waveforms) {
|
|
2576
|
+
if (!waveforms) {
|
|
2577
|
+
return { error: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: waveforms" };
|
|
2578
|
+
}
|
|
2579
|
+
if (!Array.isArray(waveforms)) {
|
|
2580
|
+
return { error: "waveforms \u5FC5\u987B\u662F\u6570\u7EC4" };
|
|
2581
|
+
}
|
|
2582
|
+
if (waveforms.length === 0) {
|
|
2583
|
+
return { error: "waveforms \u6570\u7EC4\u4E0D\u80FD\u4E3A\u7A7A" };
|
|
2584
|
+
}
|
|
2585
|
+
if (waveforms.length > 100) {
|
|
2586
|
+
return { error: `waveforms \u6570\u7EC4\u957F\u5EA6\u8D85\u8FC7\u9650\u5236: ${waveforms.length}\uFF0C\u6700\u5927 100` };
|
|
2587
|
+
}
|
|
2588
|
+
const hexPattern = /^[0-9a-fA-F]{16}$/;
|
|
2589
|
+
for (let i = 0; i < waveforms.length; i++) {
|
|
2590
|
+
const wf = waveforms[i];
|
|
2591
|
+
if (typeof wf !== "string" || !hexPattern.test(wf)) {
|
|
2592
|
+
return { error: `\u65E0\u6548\u7684\u6CE2\u5F62\u6570\u636E [${i}]: "${wf}"\uFF0C\u5FC5\u987B\u662F16\u5B57\u7B26\u7684HEX\u5B57\u7B26\u4E32` };
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return { waveforms };
|
|
2596
|
+
}
|
|
2597
|
+
function registerControlTools(toolManager, sessionManager, wsServer) {
|
|
2598
|
+
toolManager.registerTool(
|
|
2599
|
+
"dg_set_strength",
|
|
2600
|
+
`\u8BBE\u7F6E\u8BBE\u5907\u901A\u9053\u5F3A\u5EA6\u3002\u5FC5\u987B\u5728boundToApp\u4E3Atrue\u540E\u624D\u80FD\u4F7F\u7528\u3002
|
|
2601
|
+
\u53C2\u6570\u8BF4\u660E\uFF1A
|
|
2602
|
+
- deviceId \u6216 alias: \u8BBE\u5907\u6807\u8BC6\uFF08\u4E8C\u9009\u4E00\uFF0CdeviceId\u4F18\u5148\uFF09
|
|
2603
|
+
- channel: A\u6216B\u901A\u9053
|
|
2604
|
+
- mode: increase(\u589E\u52A0)/decrease(\u51CF\u5C11)/set(\u76F4\u63A5\u8BBE\u7F6E)
|
|
2605
|
+
- value: \u5F3A\u5EA6\u503C0-200\uFF0C\u4F46\u5B9E\u9645\u4E0D\u80FD\u8D85\u8FC7strengthLimit
|
|
2606
|
+
\u4F7F\u7528\u524D\u8BF7\u5148\u7528dg_get_status\u786E\u8BA4\u8BBE\u5907\u5DF2\u7ED1\u5B9AAPP\u4E14\u4E86\u89E3\u5F53\u524D\u5F3A\u5EA6\u4E0A\u9650\u3002`,
|
|
2607
|
+
{
|
|
2608
|
+
type: "object",
|
|
2609
|
+
properties: {
|
|
2610
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2611
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" },
|
|
2612
|
+
channel: { type: "string", enum: ["A", "B"], description: "\u901A\u9053" },
|
|
2613
|
+
mode: { type: "string", enum: ["increase", "decrease", "set"], description: "\u6A21\u5F0F" },
|
|
2614
|
+
value: { type: "number", minimum: 0, maximum: 200, description: "\u5F3A\u5EA6\u503C" }
|
|
2615
|
+
},
|
|
2616
|
+
required: ["channel", "mode", "value"]
|
|
2617
|
+
},
|
|
2618
|
+
async (params) => {
|
|
2619
|
+
const deviceResult = resolveDevice(
|
|
2620
|
+
sessionManager,
|
|
2621
|
+
params.deviceId,
|
|
2622
|
+
params.alias
|
|
2623
|
+
);
|
|
2624
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2625
|
+
const session = deviceResult.session;
|
|
2626
|
+
const channelResult = validateChannel(params.channel);
|
|
2627
|
+
if ("error" in channelResult) return createToolError(channelResult.error);
|
|
2628
|
+
const channel = channelResult.channel;
|
|
2629
|
+
const modeResult = validateStrengthMode(params.mode);
|
|
2630
|
+
if ("error" in modeResult) return createToolError(modeResult.error);
|
|
2631
|
+
const mode = modeResult.mode;
|
|
2632
|
+
const valueResult = validateStrengthValue(params.value);
|
|
2633
|
+
if ("error" in valueResult) return createToolError(valueResult.error);
|
|
2634
|
+
const value = valueResult.value;
|
|
2635
|
+
if (!session.clientId) {
|
|
2636
|
+
return createToolError("\u8BBE\u5907\u672A\u8FDE\u63A5");
|
|
2637
|
+
}
|
|
2638
|
+
const isBound = wsServer.isControllerBound(session.clientId);
|
|
2639
|
+
if (!isBound) {
|
|
2640
|
+
return createToolError("\u8BBE\u5907\u672A\u7ED1\u5B9AAPP");
|
|
2641
|
+
}
|
|
2642
|
+
const success = wsServer.sendStrength(session.clientId, channel, mode, value);
|
|
2643
|
+
if (!success) {
|
|
2644
|
+
return createToolError("\u53D1\u9001\u5F3A\u5EA6\u547D\u4EE4\u5931\u8D25");
|
|
2645
|
+
}
|
|
2646
|
+
sessionManager.touchSession(session.deviceId);
|
|
2647
|
+
const updated = sessionManager.getSession(session.deviceId);
|
|
2648
|
+
const newStrength = channel === "A" ? updated?.strengthA : updated?.strengthB;
|
|
2649
|
+
return createToolResult(
|
|
2650
|
+
JSON.stringify({
|
|
2651
|
+
success: true,
|
|
2652
|
+
deviceId: session.deviceId,
|
|
2653
|
+
channel,
|
|
2654
|
+
currentStrength: newStrength
|
|
2655
|
+
})
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
);
|
|
2659
|
+
toolManager.registerTool(
|
|
2660
|
+
"dg_send_waveform",
|
|
2661
|
+
`\u53D1\u9001\u6CE2\u5F62\u6570\u636E\u5230\u8BBE\u5907\uFF0C\u63A7\u5236\u8F93\u51FA\u6A21\u5F0F\u3002\u5FC5\u987B\u5728boundToApp\u4E3Atrue\u540E\u624D\u80FD\u4F7F\u7528\u3002
|
|
2662
|
+
\u652F\u6301\u4E24\u79CD\u65B9\u5F0F\uFF1A
|
|
2663
|
+
1. \u76F4\u63A5\u63D0\u4F9Bwaveforms\u6570\u7EC4\uFF08\u6BCF\u9879\u4E3A16\u5B57\u7B26HEX\u5B57\u7B26\u4E32\uFF0C\u6700\u591A100\u9879\uFF09
|
|
2664
|
+
2. \u63D0\u4F9BwaveformName\u5F15\u7528\u5DF2\u4FDD\u5B58\u7684\u6CE2\u5F62\uFF08\u901A\u8FC7dg_parse_waveform\u4FDD\u5B58\uFF09
|
|
2665
|
+
\u4E24\u79CD\u65B9\u5F0F\u4E8C\u9009\u4E00\uFF0C\u5982\u679C\u540C\u65F6\u63D0\u4F9B\u5219\u4F18\u5148\u4F7F\u7528waveforms\u3002
|
|
2666
|
+
\u6CE2\u5F62\u4F1A\u6309\u987A\u5E8F\u64AD\u653E\uFF0C\u64AD\u653E\u5B8C\u6BD5\u540E\u505C\u6B62\u3002`,
|
|
2667
|
+
{
|
|
2668
|
+
type: "object",
|
|
2669
|
+
properties: {
|
|
2670
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2671
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" },
|
|
2672
|
+
channel: { type: "string", enum: ["A", "B"], description: "\u901A\u9053" },
|
|
2673
|
+
waveforms: {
|
|
2674
|
+
type: "array",
|
|
2675
|
+
items: { type: "string" },
|
|
2676
|
+
maxItems: 100,
|
|
2677
|
+
description: "\u6CE2\u5F62\u6570\u636E\u6570\u7EC4\uFF0C\u6BCF\u9879\u4E3A8\u5B57\u8282HEX\u5B57\u7B26\u4E32\uFF0816\u4E2A\u5341\u516D\u8FDB\u5236\u5B57\u7B26\uFF09\u3002\u4E0EwaveformName\u4E8C\u9009\u4E00"
|
|
2678
|
+
},
|
|
2679
|
+
waveformName: {
|
|
2680
|
+
type: "string",
|
|
2681
|
+
description: "\u5DF2\u4FDD\u5B58\u7684\u6CE2\u5F62\u540D\u79F0\uFF08\u901A\u8FC7dg_parse_waveform\u4FDD\u5B58\uFF09\u3002\u4E0Ewaveforms\u4E8C\u9009\u4E00"
|
|
2682
|
+
}
|
|
2683
|
+
},
|
|
2684
|
+
required: ["channel"]
|
|
2685
|
+
},
|
|
2686
|
+
async (params) => {
|
|
2687
|
+
const deviceResult = resolveDevice(
|
|
2688
|
+
sessionManager,
|
|
2689
|
+
params.deviceId,
|
|
2690
|
+
params.alias
|
|
2691
|
+
);
|
|
2692
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2693
|
+
const session = deviceResult.session;
|
|
2694
|
+
const channelResult = validateChannel(params.channel);
|
|
2695
|
+
if ("error" in channelResult) return createToolError(channelResult.error);
|
|
2696
|
+
const channel = channelResult.channel;
|
|
2697
|
+
const rawWaveforms = params.waveforms;
|
|
2698
|
+
const waveformName = params.waveformName;
|
|
2699
|
+
if (!rawWaveforms && !waveformName) {
|
|
2700
|
+
return createToolError("\u5FC5\u987B\u63D0\u4F9B waveforms \u6216 waveformName \u53C2\u6570\u4E4B\u4E00");
|
|
2701
|
+
}
|
|
2702
|
+
let waveforms;
|
|
2703
|
+
if (rawWaveforms) {
|
|
2704
|
+
const waveformsResult = validateWaveforms(rawWaveforms);
|
|
2705
|
+
if ("error" in waveformsResult) return createToolError(waveformsResult.error);
|
|
2706
|
+
waveforms = waveformsResult.waveforms;
|
|
2707
|
+
} else {
|
|
2708
|
+
const storage = getWaveformStorage();
|
|
2709
|
+
const storedWaveform = storage.get(waveformName);
|
|
2710
|
+
if (!storedWaveform) {
|
|
2711
|
+
return createToolError(`\u6CE2\u5F62\u4E0D\u5B58\u5728: ${waveformName}`);
|
|
2712
|
+
}
|
|
2713
|
+
waveforms = storedWaveform.hexWaveforms;
|
|
2714
|
+
}
|
|
2715
|
+
if (!session.clientId) {
|
|
2716
|
+
return createToolError("\u8BBE\u5907\u672A\u8FDE\u63A5");
|
|
2717
|
+
}
|
|
2718
|
+
const isBound = wsServer.isControllerBound(session.clientId);
|
|
2719
|
+
if (!isBound) {
|
|
2720
|
+
return createToolError("\u8BBE\u5907\u672A\u7ED1\u5B9AAPP");
|
|
2721
|
+
}
|
|
2722
|
+
const success = wsServer.sendWaveform(session.clientId, channel, waveforms);
|
|
2723
|
+
if (!success) {
|
|
2724
|
+
return createToolError("\u53D1\u9001\u6CE2\u5F62\u6570\u636E\u5931\u8D25");
|
|
2725
|
+
}
|
|
2726
|
+
sessionManager.touchSession(session.deviceId);
|
|
2727
|
+
return createToolResult(
|
|
2728
|
+
JSON.stringify({
|
|
2729
|
+
success: true,
|
|
2730
|
+
deviceId: session.deviceId,
|
|
2731
|
+
channel,
|
|
2732
|
+
waveformCount: waveforms.length,
|
|
2733
|
+
source: rawWaveforms ? "direct" : `waveform:${waveformName}`
|
|
2734
|
+
})
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
);
|
|
2738
|
+
toolManager.registerTool(
|
|
2739
|
+
"dg_clear_waveform",
|
|
2740
|
+
`\u6E05\u7A7A\u8BBE\u5907\u6307\u5B9A\u901A\u9053\u7684\u6CE2\u5F62\u961F\u5217\uFF0C\u7ACB\u5373\u505C\u6B62\u5F53\u524D\u6CE2\u5F62\u64AD\u653E\u3002
|
|
2741
|
+
\u7528\u4E8E\u4E2D\u65AD\u6B63\u5728\u64AD\u653E\u7684\u6CE2\u5F62\u6216\u5728\u53D1\u9001\u65B0\u6CE2\u5F62\u524D\u6E05\u7A7A\u961F\u5217\u3002`,
|
|
2742
|
+
{
|
|
2743
|
+
type: "object",
|
|
2744
|
+
properties: {
|
|
2745
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2746
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" },
|
|
2747
|
+
channel: { type: "string", enum: ["A", "B"], description: "\u901A\u9053" }
|
|
2748
|
+
},
|
|
2749
|
+
required: ["channel"]
|
|
2750
|
+
},
|
|
2751
|
+
async (params) => {
|
|
2752
|
+
const deviceResult = resolveDevice(
|
|
2753
|
+
sessionManager,
|
|
2754
|
+
params.deviceId,
|
|
2755
|
+
params.alias
|
|
2756
|
+
);
|
|
2757
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2758
|
+
const session = deviceResult.session;
|
|
2759
|
+
const channelResult = validateChannel(params.channel);
|
|
2760
|
+
if ("error" in channelResult) return createToolError(channelResult.error);
|
|
2761
|
+
const channel = channelResult.channel;
|
|
2762
|
+
if (!session.clientId) {
|
|
2763
|
+
return createToolError("\u8BBE\u5907\u672A\u8FDE\u63A5");
|
|
2764
|
+
}
|
|
2765
|
+
const isBound = wsServer.isControllerBound(session.clientId);
|
|
2766
|
+
if (!isBound) {
|
|
2767
|
+
return createToolError("\u8BBE\u5907\u672A\u7ED1\u5B9AAPP");
|
|
2768
|
+
}
|
|
2769
|
+
const success = wsServer.clearWaveform(session.clientId, channel);
|
|
2770
|
+
if (!success) {
|
|
2771
|
+
return createToolError("\u6E05\u7A7A\u6CE2\u5F62\u961F\u5217\u5931\u8D25");
|
|
2772
|
+
}
|
|
2773
|
+
sessionManager.touchSession(session.deviceId);
|
|
2774
|
+
return createToolResult(
|
|
2775
|
+
JSON.stringify({
|
|
2776
|
+
success: true,
|
|
2777
|
+
deviceId: session.deviceId,
|
|
2778
|
+
channel
|
|
2779
|
+
})
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
);
|
|
2783
|
+
toolManager.registerTool(
|
|
2784
|
+
"dg_get_status",
|
|
2785
|
+
`\u83B7\u53D6\u8BBE\u5907\u5B8C\u6574\u72B6\u6001\u4FE1\u606F\u3002
|
|
2786
|
+
\u5173\u952E\u5B57\u6BB5\uFF1A
|
|
2787
|
+
- boundToApp: \u662F\u5426\u5DF2\u7ED1\u5B9AAPP\uFF08\u5FC5\u987B\u4E3Atrue\u624D\u80FD\u63A7\u5236\u8BBE\u5907\uFF09
|
|
2788
|
+
- strengthA/B: \u5F53\u524DA/B\u901A\u9053\u5F3A\u5EA6
|
|
2789
|
+
- strengthLimitA/B: A/B\u901A\u9053\u5F3A\u5EA6\u4E0A\u9650\uFF08\u7531APP\u8BBE\u7F6E\uFF0C\u4E0D\u53EF\u8D85\u8FC7\uFF09
|
|
2790
|
+
\u5EFA\u8BAE\u5728dg_connect\u540E\u5728\u7528\u6237\u8BF4\u5DF2\u5B8C\u6210\u540E\u4F7F\u7528\u6B64\u63A5\u53E3\u68C0\u67E5boundToApp\u72B6\u6001\u3002`,
|
|
2791
|
+
{
|
|
2792
|
+
type: "object",
|
|
2793
|
+
properties: {
|
|
2794
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2795
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" }
|
|
2796
|
+
},
|
|
2797
|
+
required: []
|
|
2798
|
+
},
|
|
2799
|
+
async (params) => {
|
|
2800
|
+
const deviceResult = resolveDevice(
|
|
2801
|
+
sessionManager,
|
|
2802
|
+
params.deviceId,
|
|
2803
|
+
params.alias
|
|
2804
|
+
);
|
|
2805
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2806
|
+
const session = deviceResult.session;
|
|
2807
|
+
const isBound = session.clientId ? wsServer.isControllerBound(session.clientId) : false;
|
|
2808
|
+
return createToolResult(
|
|
2809
|
+
JSON.stringify({
|
|
2810
|
+
deviceId: session.deviceId,
|
|
2811
|
+
alias: session.alias,
|
|
2812
|
+
connected: session.connected,
|
|
2813
|
+
boundToApp: isBound,
|
|
2814
|
+
strengthA: session.strengthA,
|
|
2815
|
+
strengthB: session.strengthB,
|
|
2816
|
+
strengthLimitA: session.strengthLimitA,
|
|
2817
|
+
strengthLimitB: session.strengthLimitB
|
|
2818
|
+
})
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
);
|
|
2822
|
+
toolManager.registerTool(
|
|
2823
|
+
"dg_start_continuous_playback",
|
|
2824
|
+
`\u542F\u52A8\u6301\u7EED\u64AD\u653E\u6A21\u5F0F\uFF0C\u5FAA\u73AF\u53D1\u9001\u6CE2\u5F62\u6570\u636E\u76F4\u5230\u624B\u52A8\u505C\u6B62\u3002
|
|
2825
|
+
\u4E0Edg_send_waveform\u4E0D\u540C\uFF0C\u6301\u7EED\u64AD\u653E\u4F1A\u81EA\u52A8\u5FAA\u73AF\u53D1\u9001\u6CE2\u5F62\uFF0C\u9002\u5408\u9700\u8981\u6301\u7EED\u8F93\u51FA\u7684\u573A\u666F\u3002
|
|
2826
|
+
\u652F\u6301\u4E24\u79CD\u65B9\u5F0F\u63D0\u4F9B\u6CE2\u5F62\uFF1A
|
|
2827
|
+
1. \u76F4\u63A5\u63D0\u4F9Bwaveforms\u6570\u7EC4
|
|
2828
|
+
2. \u63D0\u4F9BwaveformName\u5F15\u7528\u5DF2\u4FDD\u5B58\u7684\u6CE2\u5F62
|
|
2829
|
+
\u53EF\u9009\u53C2\u6570\uFF1A
|
|
2830
|
+
- interval: \u53D1\u9001\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4100ms
|
|
2831
|
+
- batchSize: \u6BCF\u6B21\u53D1\u9001\u7684\u6CE2\u5F62\u6570\u91CF\uFF0C\u9ED8\u8BA45`,
|
|
2832
|
+
{
|
|
2833
|
+
type: "object",
|
|
2834
|
+
properties: {
|
|
2835
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2836
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" },
|
|
2837
|
+
channel: { type: "string", enum: ["A", "B"], description: "\u901A\u9053" },
|
|
2838
|
+
waveforms: {
|
|
2839
|
+
type: "array",
|
|
2840
|
+
items: { type: "string" },
|
|
2841
|
+
maxItems: 100,
|
|
2842
|
+
description: "\u6CE2\u5F62\u6570\u636E\u6570\u7EC4\uFF0C\u6BCF\u9879\u4E3A8\u5B57\u8282HEX\u5B57\u7B26\u4E32\u3002\u4E0EwaveformName\u4E8C\u9009\u4E00"
|
|
2843
|
+
},
|
|
2844
|
+
waveformName: {
|
|
2845
|
+
type: "string",
|
|
2846
|
+
description: "\u5DF2\u4FDD\u5B58\u7684\u6CE2\u5F62\u540D\u79F0\u3002\u4E0Ewaveforms\u4E8C\u9009\u4E00"
|
|
2847
|
+
},
|
|
2848
|
+
interval: {
|
|
2849
|
+
type: "number",
|
|
2850
|
+
minimum: 50,
|
|
2851
|
+
maximum: 5e3,
|
|
2852
|
+
description: "\u53D1\u9001\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4100"
|
|
2853
|
+
},
|
|
2854
|
+
batchSize: {
|
|
2855
|
+
type: "number",
|
|
2856
|
+
minimum: 1,
|
|
2857
|
+
maximum: 20,
|
|
2858
|
+
description: "\u6BCF\u6B21\u53D1\u9001\u7684\u6CE2\u5F62\u6570\u91CF\uFF0C\u9ED8\u8BA45"
|
|
2859
|
+
}
|
|
2860
|
+
},
|
|
2861
|
+
required: ["channel"]
|
|
2862
|
+
},
|
|
2863
|
+
async (params) => {
|
|
2864
|
+
const deviceResult = resolveDevice(
|
|
2865
|
+
sessionManager,
|
|
2866
|
+
params.deviceId,
|
|
2867
|
+
params.alias
|
|
2868
|
+
);
|
|
2869
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2870
|
+
const session = deviceResult.session;
|
|
2871
|
+
const channelResult = validateChannel(params.channel);
|
|
2872
|
+
if ("error" in channelResult) return createToolError(channelResult.error);
|
|
2873
|
+
const channel = channelResult.channel;
|
|
2874
|
+
const rawWaveforms = params.waveforms;
|
|
2875
|
+
const waveformName = params.waveformName;
|
|
2876
|
+
if (!rawWaveforms && !waveformName) {
|
|
2877
|
+
return createToolError("\u5FC5\u987B\u63D0\u4F9B waveforms \u6216 waveformName \u53C2\u6570\u4E4B\u4E00");
|
|
2878
|
+
}
|
|
2879
|
+
let waveforms;
|
|
2880
|
+
if (rawWaveforms) {
|
|
2881
|
+
const waveformsResult = validateWaveforms(rawWaveforms);
|
|
2882
|
+
if ("error" in waveformsResult) return createToolError(waveformsResult.error);
|
|
2883
|
+
waveforms = waveformsResult.waveforms;
|
|
2884
|
+
} else {
|
|
2885
|
+
const storage = getWaveformStorage();
|
|
2886
|
+
const storedWaveform = storage.get(waveformName);
|
|
2887
|
+
if (!storedWaveform) {
|
|
2888
|
+
return createToolError(`\u6CE2\u5F62\u4E0D\u5B58\u5728: ${waveformName}`);
|
|
2889
|
+
}
|
|
2890
|
+
waveforms = storedWaveform.hexWaveforms;
|
|
2891
|
+
}
|
|
2892
|
+
if (!session.clientId) {
|
|
2893
|
+
return createToolError("\u8BBE\u5907\u672A\u8FDE\u63A5");
|
|
2894
|
+
}
|
|
2895
|
+
const isBound = wsServer.isControllerBound(session.clientId);
|
|
2896
|
+
if (!isBound) {
|
|
2897
|
+
return createToolError("\u8BBE\u5907\u672A\u7ED1\u5B9AAPP");
|
|
2898
|
+
}
|
|
2899
|
+
const interval = typeof params.interval === "number" ? params.interval : 100;
|
|
2900
|
+
const batchSize = typeof params.batchSize === "number" ? params.batchSize : 5;
|
|
2901
|
+
const success = wsServer.startContinuousPlayback(
|
|
2902
|
+
session.clientId,
|
|
2903
|
+
channel,
|
|
2904
|
+
waveforms,
|
|
2905
|
+
interval,
|
|
2906
|
+
batchSize
|
|
2907
|
+
);
|
|
2908
|
+
if (!success) {
|
|
2909
|
+
return createToolError("\u542F\u52A8\u6301\u7EED\u64AD\u653E\u5931\u8D25");
|
|
2910
|
+
}
|
|
2911
|
+
sessionManager.touchSession(session.deviceId);
|
|
2912
|
+
return createToolResult(
|
|
2913
|
+
JSON.stringify({
|
|
2914
|
+
success: true,
|
|
2915
|
+
deviceId: session.deviceId,
|
|
2916
|
+
channel,
|
|
2917
|
+
waveformCount: waveforms.length,
|
|
2918
|
+
interval,
|
|
2919
|
+
batchSize,
|
|
2920
|
+
source: rawWaveforms ? "direct" : `waveform:${waveformName}`
|
|
2921
|
+
})
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
);
|
|
2925
|
+
toolManager.registerTool(
|
|
2926
|
+
"dg_stop_continuous_playback",
|
|
2927
|
+
`\u505C\u6B62\u6307\u5B9A\u901A\u9053\u7684\u6301\u7EED\u64AD\u653E\u3002
|
|
2928
|
+
\u4F1A\u7ACB\u5373\u505C\u6B62\u5FAA\u73AF\u53D1\u9001\u5E76\u6E05\u7A7A\u6CE2\u5F62\u961F\u5217\u3002`,
|
|
2929
|
+
{
|
|
2930
|
+
type: "object",
|
|
2931
|
+
properties: {
|
|
2932
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2933
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" },
|
|
2934
|
+
channel: { type: "string", enum: ["A", "B"], description: "\u901A\u9053" }
|
|
2935
|
+
},
|
|
2936
|
+
required: ["channel"]
|
|
2937
|
+
},
|
|
2938
|
+
async (params) => {
|
|
2939
|
+
const deviceResult = resolveDevice(
|
|
2940
|
+
sessionManager,
|
|
2941
|
+
params.deviceId,
|
|
2942
|
+
params.alias
|
|
2943
|
+
);
|
|
2944
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2945
|
+
const session = deviceResult.session;
|
|
2946
|
+
const channelResult = validateChannel(params.channel);
|
|
2947
|
+
if ("error" in channelResult) return createToolError(channelResult.error);
|
|
2948
|
+
const channel = channelResult.channel;
|
|
2949
|
+
if (!session.clientId) {
|
|
2950
|
+
return createToolError("\u8BBE\u5907\u672A\u8FDE\u63A5");
|
|
2951
|
+
}
|
|
2952
|
+
const success = wsServer.stopContinuousPlayback(session.clientId, channel);
|
|
2953
|
+
if (!success) {
|
|
2954
|
+
return createToolError("\u505C\u6B62\u6301\u7EED\u64AD\u653E\u5931\u8D25\uFF1A\u8BE5\u901A\u9053\u6CA1\u6709\u6B63\u5728\u8FDB\u884C\u7684\u6301\u7EED\u64AD\u653E");
|
|
2955
|
+
}
|
|
2956
|
+
sessionManager.touchSession(session.deviceId);
|
|
2957
|
+
return createToolResult(
|
|
2958
|
+
JSON.stringify({
|
|
2959
|
+
success: true,
|
|
2960
|
+
deviceId: session.deviceId,
|
|
2961
|
+
channel
|
|
2962
|
+
})
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
);
|
|
2966
|
+
toolManager.registerTool(
|
|
2967
|
+
"dg_get_playback_status",
|
|
2968
|
+
`\u83B7\u53D6\u8BBE\u5907\u7684\u6301\u7EED\u64AD\u653E\u72B6\u6001\u3002
|
|
2969
|
+
\u8FD4\u56DEA\u548CB\u901A\u9053\u7684\u64AD\u653E\u72B6\u6001\uFF0C\u5305\u62EC\u662F\u5426\u6B63\u5728\u64AD\u653E\u3001\u6CE2\u5F62\u6570\u91CF\u3001\u53D1\u9001\u95F4\u9694\u7B49\u4FE1\u606F\u3002`,
|
|
2970
|
+
{
|
|
2971
|
+
type: "object",
|
|
2972
|
+
properties: {
|
|
2973
|
+
deviceId: { type: "string", description: "\u8BBE\u5907ID\uFF08\u4E0Ealias\u4E8C\u9009\u4E00\uFF0C\u4F18\u5148\u4F7F\u7528\uFF09" },
|
|
2974
|
+
alias: { type: "string", description: "\u8BBE\u5907\u522B\u540D\uFF08\u4E0EdeviceId\u4E8C\u9009\u4E00\uFF09" }
|
|
2975
|
+
},
|
|
2976
|
+
required: []
|
|
2977
|
+
},
|
|
2978
|
+
async (params) => {
|
|
2979
|
+
const deviceResult = resolveDevice(
|
|
2980
|
+
sessionManager,
|
|
2981
|
+
params.deviceId,
|
|
2982
|
+
params.alias
|
|
2983
|
+
);
|
|
2984
|
+
if ("error" in deviceResult) return createToolError(deviceResult.error);
|
|
2985
|
+
const session = deviceResult.session;
|
|
2986
|
+
if (!session.clientId) {
|
|
2987
|
+
return createToolError("\u8BBE\u5907\u672A\u8FDE\u63A5");
|
|
2988
|
+
}
|
|
2989
|
+
const statusA = wsServer.getContinuousPlaybackState(session.clientId, "A");
|
|
2990
|
+
const statusB = wsServer.getContinuousPlaybackState(session.clientId, "B");
|
|
2991
|
+
return createToolResult(
|
|
2992
|
+
JSON.stringify({
|
|
2993
|
+
deviceId: session.deviceId,
|
|
2994
|
+
channelA: statusA ? {
|
|
2995
|
+
playing: statusA.active,
|
|
2996
|
+
waveformCount: statusA.waveformCount,
|
|
2997
|
+
interval: statusA.interval,
|
|
2998
|
+
batchSize: statusA.batchSize
|
|
2999
|
+
} : { playing: false },
|
|
3000
|
+
channelB: statusB ? {
|
|
3001
|
+
playing: statusB.active,
|
|
3002
|
+
waveformCount: statusB.waveformCount,
|
|
3003
|
+
interval: statusB.interval,
|
|
3004
|
+
batchSize: statusB.batchSize
|
|
3005
|
+
} : { playing: false }
|
|
3006
|
+
})
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// src/app.ts
|
|
3013
|
+
function createApp() {
|
|
3014
|
+
const config = loadConfig();
|
|
3015
|
+
printConfigInfo(config);
|
|
3016
|
+
const server = createServer(config);
|
|
3017
|
+
const toolManager = new ToolManager(() => {
|
|
3018
|
+
broadcastNotification(server, "notifications/tools/list_changed");
|
|
3019
|
+
});
|
|
3020
|
+
const sessionManager = new SessionManager(config.connectionTimeoutMinutes);
|
|
3021
|
+
console.log(`[\u4F1A\u8BDD] \u4EC5\u5185\u5B58\u6A21\u5F0F\uFF08\u8FDE\u63A5\u8D85\u65F6: ${config.connectionTimeoutMinutes} \u5206\u949F\uFF0C\u6D3B\u8DC3\u8D85\u65F6: 1 \u5C0F\u65F6\uFF09`);
|
|
3022
|
+
const wsServer = createWSServer(config, sessionManager);
|
|
3023
|
+
const waveformStorage2 = initWaveforms(config);
|
|
3024
|
+
registerProtocolAndTools(server, toolManager, sessionManager, wsServer, config);
|
|
3025
|
+
const shutdown = async () => {
|
|
3026
|
+
console.log("\n[\u670D\u52A1\u5668] \u6B63\u5728\u5173\u95ED...");
|
|
3027
|
+
wsServer.stop();
|
|
3028
|
+
sessionManager.stopCleanupTimer();
|
|
3029
|
+
sessionManager.clearAll();
|
|
3030
|
+
await server.stop();
|
|
3031
|
+
console.log("[\u670D\u52A1\u5668] \u5DF2\u505C\u6B62");
|
|
3032
|
+
};
|
|
3033
|
+
return {
|
|
3034
|
+
config,
|
|
3035
|
+
server,
|
|
3036
|
+
toolManager,
|
|
3037
|
+
sessionManager,
|
|
3038
|
+
wsServer,
|
|
3039
|
+
waveformStorage: waveformStorage2,
|
|
3040
|
+
shutdown
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
function printConfigInfo(config) {
|
|
3044
|
+
console.log("=".repeat(50));
|
|
3045
|
+
console.log("DG-LAB MCP SSE \u670D\u52A1\u5668");
|
|
3046
|
+
console.log("=".repeat(50));
|
|
3047
|
+
console.log(`[\u914D\u7F6E] \u7AEF\u53E3: ${config.port}`);
|
|
3048
|
+
console.log(`[\u914D\u7F6E] SSE \u8DEF\u5F84: ${config.ssePath}`);
|
|
3049
|
+
console.log(`[\u914D\u7F6E] POST \u8DEF\u5F84: ${config.postPath}`);
|
|
3050
|
+
const effectiveIP = getEffectiveIP(config);
|
|
3051
|
+
const localIP = getLocalIP();
|
|
3052
|
+
console.log(`[\u914D\u7F6E] \u672C\u5730 IP: ${localIP}`);
|
|
3053
|
+
console.log(`[\u914D\u7F6E] \u516C\u7F51 IP: ${config.publicIp || "(\u672A\u8BBE\u7F6E)"}`);
|
|
3054
|
+
console.log(`[\u914D\u7F6E] \u5B9E\u9645\u4F7F\u7528 IP: ${effectiveIP}`);
|
|
3055
|
+
}
|
|
3056
|
+
function createWSServer(config, sessionManager) {
|
|
3057
|
+
return new DGLabWSServer({
|
|
3058
|
+
heartbeatInterval: config.heartbeatInterval,
|
|
3059
|
+
onStrengthUpdate: (controllerId, a, b, limitA, limitB) => {
|
|
3060
|
+
console.log(`[WS] ${controllerId} \u5F3A\u5EA6: A=${a}/${limitA}, B=${b}/${limitB}`);
|
|
3061
|
+
const session = sessionManager.getSessionByClientId(controllerId);
|
|
3062
|
+
if (session) {
|
|
3063
|
+
sessionManager.updateStrength(session.deviceId, a, b, limitA, limitB);
|
|
3064
|
+
}
|
|
3065
|
+
},
|
|
3066
|
+
onFeedback: (controllerId, index) => {
|
|
3067
|
+
console.log(`[WS] ${controllerId} \u53CD\u9988: ${index}`);
|
|
3068
|
+
},
|
|
3069
|
+
onBindChange: (controllerId, appId) => {
|
|
3070
|
+
console.log(`[WS] ${controllerId} \u7ED1\u5B9A: ${appId || "\u5DF2\u89E3\u7ED1"}`);
|
|
3071
|
+
const session = sessionManager.getSessionByClientId(controllerId);
|
|
3072
|
+
if (session) {
|
|
3073
|
+
sessionManager.updateConnectionState(session.deviceId, {
|
|
3074
|
+
boundToApp: !!appId,
|
|
3075
|
+
targetId: appId
|
|
3076
|
+
});
|
|
3077
|
+
if (appId) {
|
|
3078
|
+
sessionManager.onAppBound(session.deviceId);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
},
|
|
3082
|
+
onControllerDisconnect: (controllerId) => {
|
|
3083
|
+
console.log(`[WS] \u63A7\u5236\u5668\u65AD\u5F00: ${controllerId}`);
|
|
3084
|
+
const session = sessionManager.getSessionByClientId(controllerId);
|
|
3085
|
+
if (session) {
|
|
3086
|
+
sessionManager.updateConnectionState(session.deviceId, {
|
|
3087
|
+
connected: false,
|
|
3088
|
+
boundToApp: false,
|
|
3089
|
+
clientId: null,
|
|
3090
|
+
targetId: null
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
},
|
|
3094
|
+
onAppDisconnect: (appId) => {
|
|
3095
|
+
console.log(`[WS] APP \u65AD\u5F00: ${appId}`);
|
|
3096
|
+
const sessions = sessionManager.listSessions();
|
|
3097
|
+
for (const session of sessions) {
|
|
3098
|
+
if (session.targetId === appId) {
|
|
3099
|
+
sessionManager.updateConnectionState(session.deviceId, {
|
|
3100
|
+
boundToApp: false,
|
|
3101
|
+
targetId: null
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
function initWaveforms(config) {
|
|
3109
|
+
const waveformStorage2 = new WaveformStorage();
|
|
3110
|
+
if (loadWaveforms(waveformStorage2, config.waveformStorePath)) {
|
|
3111
|
+
console.log(`[\u6CE2\u5F62] \u4ECE\u78C1\u76D8\u52A0\u8F7D\u4E86 ${waveformStorage2.list().length} \u4E2A\u6CE2\u5F62`);
|
|
3112
|
+
}
|
|
3113
|
+
initWaveformStorage(waveformStorage2, config.waveformStorePath);
|
|
3114
|
+
return waveformStorage2;
|
|
3115
|
+
}
|
|
3116
|
+
function registerProtocolAndTools(server, toolManager, sessionManager, wsServer, config) {
|
|
3117
|
+
registerMCPProtocol(server.jsonRpcHandler, () => {
|
|
3118
|
+
console.log("[MCP] \u5BA2\u6237\u7AEF\u5DF2\u521D\u59CB\u5316");
|
|
3119
|
+
});
|
|
3120
|
+
registerToolHandlers(server.jsonRpcHandler, toolManager);
|
|
3121
|
+
registerDeviceTools(toolManager, sessionManager, wsServer, config.publicIp || void 0);
|
|
3122
|
+
console.log("[\u5DE5\u5177] \u8BBE\u5907\u5DE5\u5177\u5DF2\u6CE8\u518C");
|
|
3123
|
+
registerControlTools(toolManager, sessionManager, wsServer);
|
|
3124
|
+
console.log("[\u5DE5\u5177] \u63A7\u5236\u5DE5\u5177\u5DF2\u6CE8\u518C");
|
|
3125
|
+
const waveformTools = getWaveformTools();
|
|
3126
|
+
for (const tool of waveformTools) {
|
|
3127
|
+
toolManager.registerTool(tool.name, tool.description, tool.inputSchema, tool.handler);
|
|
3128
|
+
}
|
|
3129
|
+
console.log("[\u5DE5\u5177] \u6CE2\u5F62\u5DE5\u5177\u5DF2\u6CE8\u518C");
|
|
3130
|
+
console.log(`[\u5DE5\u5177] \u603B\u8BA1: ${toolManager.toolCount}`);
|
|
3131
|
+
}
|
|
3132
|
+
async function startApp(app) {
|
|
3133
|
+
await app.server.start();
|
|
3134
|
+
if (app.server.httpServer) {
|
|
3135
|
+
app.wsServer.attachToServer(app.server.httpServer, app.config.port);
|
|
3136
|
+
} else {
|
|
3137
|
+
throw new ConfigError("HTTP \u670D\u52A1\u5668\u672A\u542F\u52A8\uFF0C\u65E0\u6CD5\u9644\u52A0 WebSocket", {
|
|
3138
|
+
code: "CONFIG_LOAD_FAILED" /* CONFIG_LOAD_FAILED */,
|
|
3139
|
+
context: { port: app.config.port }
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
console.log("=".repeat(50));
|
|
3143
|
+
console.log("\u670D\u52A1\u5668\u5C31\u7EEA");
|
|
3144
|
+
console.log(`SSE: http://localhost:${app.config.port}${app.config.ssePath}`);
|
|
3145
|
+
console.log(`POST: http://localhost:${app.config.port}${app.config.postPath}`);
|
|
3146
|
+
console.log(`WebSocket: ws://localhost:${app.config.port}`);
|
|
3147
|
+
console.log("=".repeat(50));
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// src/cli.ts
|
|
3151
|
+
async function main() {
|
|
3152
|
+
const app = createApp();
|
|
3153
|
+
const shutdown = async () => {
|
|
3154
|
+
await app.shutdown();
|
|
3155
|
+
process.exit(0);
|
|
3156
|
+
};
|
|
3157
|
+
process.on("SIGINT", shutdown);
|
|
3158
|
+
process.on("SIGTERM", shutdown);
|
|
3159
|
+
await startApp(app);
|
|
3160
|
+
}
|
|
3161
|
+
main().catch((error) => {
|
|
3162
|
+
console.error("[\u81F4\u547D\u9519\u8BEF]", error);
|
|
3163
|
+
process.exit(1);
|
|
3164
|
+
});
|
|
3165
|
+
//# sourceMappingURL=cli.js.map
|