record_safer_pro_mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -0
- package/dist/api-client.d.ts +159 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +991 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/index.d.ts +12 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +235 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/index.d.ts +7 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +59 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/mixing-guide-content.d.ts +8 -0
- package/dist/resources/mixing-guide-content.d.ts.map +1 -0
- package/dist/resources/mixing-guide-content.js +194 -0
- package/dist/resources/mixing-guide-content.js.map +1 -0
- package/dist/runtime.d.ts +39 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +320 -0
- package/dist/runtime.js.map +1 -0
- package/dist/telemetry-schema.d.ts +39 -0
- package/dist/telemetry-schema.d.ts.map +1 -0
- package/dist/telemetry-schema.js +15 -0
- package/dist/telemetry-schema.js.map +1 -0
- package/dist/tools/core.d.ts +4 -0
- package/dist/tools/core.d.ts.map +1 -0
- package/dist/tools/core.js +360 -0
- package/dist/tools/core.js.map +1 -0
- package/dist/tools/devices.d.ts +10 -0
- package/dist/tools/devices.d.ts.map +1 -0
- package/dist/tools/devices.js +53 -0
- package/dist/tools/devices.js.map +1 -0
- package/dist/tools/levels.d.ts +11 -0
- package/dist/tools/levels.d.ts.map +1 -0
- package/dist/tools/levels.js +25 -0
- package/dist/tools/levels.js.map +1 -0
- package/dist/tools/markers.d.ts +17 -0
- package/dist/tools/markers.d.ts.map +1 -0
- package/dist/tools/markers.js +142 -0
- package/dist/tools/markers.js.map +1 -0
- package/dist/tools/monitor.d.ts +19 -0
- package/dist/tools/monitor.d.ts.map +1 -0
- package/dist/tools/monitor.js +232 -0
- package/dist/tools/monitor.js.map +1 -0
- package/dist/tools/recording.d.ts +12 -0
- package/dist/tools/recording.d.ts.map +1 -0
- package/dist/tools/recording.js +85 -0
- package/dist/tools/recording.js.map +1 -0
- package/dist/tools/settings.d.ts +10 -0
- package/dist/tools/settings.d.ts.map +1 -0
- package/dist/tools/settings.js +43 -0
- package/dist/tools/settings.js.map +1 -0
- package/dist/tools/storage.d.ts +10 -0
- package/dist/tools/storage.d.ts.map +1 -0
- package/dist/tools/storage.js +20 -0
- package/dist/tools/storage.js.map +1 -0
- package/dist/tools/system.d.ts +10 -0
- package/dist/tools/system.d.ts.map +1 -0
- package/dist/tools/system.js +53 -0
- package/dist/tools/system.js.map +1 -0
- package/dist/tools/tool-metadata.d.ts +19 -0
- package/dist/tools/tool-metadata.d.ts.map +1 -0
- package/dist/tools/tool-metadata.js +25 -0
- package/dist/tools/tool-metadata.js.map +1 -0
- package/dist/types.d.ts +456 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +80 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +336 -0
- package/dist/utils.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record Safer API Client — 统一 HTTP 客户端适配层
|
|
3
|
+
* 封装与 Backend (record_safer_headless) 的所有 HTTP 通信
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
/** Backend HTTP 错误,包含状态码和响应体 */
|
|
10
|
+
export class ApiError extends Error {
|
|
11
|
+
statusCode;
|
|
12
|
+
responseBody;
|
|
13
|
+
constructor(statusCode, responseBody) {
|
|
14
|
+
super(buildSafeHttpErrorMessage(statusCode));
|
|
15
|
+
this.statusCode = statusCode;
|
|
16
|
+
this.responseBody = responseBody;
|
|
17
|
+
this.name = "ApiError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function buildSafeHttpErrorMessage(statusCode) {
|
|
21
|
+
if (statusCode === 400) {
|
|
22
|
+
return "Backend rejected the request as invalid (HTTP 400).";
|
|
23
|
+
}
|
|
24
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
25
|
+
return `Backend authentication or authorization failed (HTTP ${statusCode}).`;
|
|
26
|
+
}
|
|
27
|
+
if (statusCode === 404) {
|
|
28
|
+
return "Backend endpoint was not found (HTTP 404).";
|
|
29
|
+
}
|
|
30
|
+
if (statusCode === 409) {
|
|
31
|
+
return "Backend rejected the request due to a state conflict (HTTP 409).";
|
|
32
|
+
}
|
|
33
|
+
if (statusCode === 422) {
|
|
34
|
+
return "Backend could not validate the request (HTTP 422).";
|
|
35
|
+
}
|
|
36
|
+
if (statusCode >= 500) {
|
|
37
|
+
return `Backend encountered an internal error (HTTP ${statusCode}).`;
|
|
38
|
+
}
|
|
39
|
+
return `Backend request failed (HTTP ${statusCode}).`;
|
|
40
|
+
}
|
|
41
|
+
export function normalizeDisplayPath(filePath) {
|
|
42
|
+
const trimmed = filePath.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
const normalizedPath = path.normalize(trimmed);
|
|
47
|
+
const normalizedHome = path.normalize(os.homedir());
|
|
48
|
+
if (normalizedPath === normalizedHome) {
|
|
49
|
+
return "~";
|
|
50
|
+
}
|
|
51
|
+
if (normalizedPath.startsWith(`${normalizedHome}${path.sep}`)) {
|
|
52
|
+
return `~${normalizedPath.slice(normalizedHome.length)}`;
|
|
53
|
+
}
|
|
54
|
+
return normalizedPath;
|
|
55
|
+
}
|
|
56
|
+
function parseTruthyEnv(value) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const normalized = value.trim().toLowerCase();
|
|
61
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
62
|
+
}
|
|
63
|
+
function normalizeHostEntry(entry) {
|
|
64
|
+
const trimmed = entry.trim();
|
|
65
|
+
if (!trimmed) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(trimmed.includes("://") ? trimmed : `http://${trimmed}`);
|
|
70
|
+
return parsed.hostname.toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function getRemoteFileTokenAllowlist() {
|
|
77
|
+
const allowlistRaw = process.env.RECORD_SAFER_REMOTE_FILE_TOKEN_HOSTS;
|
|
78
|
+
if (!allowlistRaw) {
|
|
79
|
+
return new Set();
|
|
80
|
+
}
|
|
81
|
+
return new Set(allowlistRaw
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((entry) => normalizeHostEntry(entry))
|
|
84
|
+
.filter((entry) => Boolean(entry)));
|
|
85
|
+
}
|
|
86
|
+
function isLoopbackHost(hostname) {
|
|
87
|
+
const normalized = hostname.toLowerCase();
|
|
88
|
+
return (normalized === "localhost" ||
|
|
89
|
+
normalized === "::1" ||
|
|
90
|
+
normalized === "[::1]" ||
|
|
91
|
+
normalized === "127.0.0.1" ||
|
|
92
|
+
normalized.startsWith("127.") ||
|
|
93
|
+
normalized.startsWith("::ffff:127."));
|
|
94
|
+
}
|
|
95
|
+
function isLoopbackBaseUrl(rawUrl) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL(rawUrl);
|
|
98
|
+
return isLoopbackHost(parsed.hostname);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function isRemoteFileTokenAllowed(rawUrl) {
|
|
105
|
+
if (!parseTruthyEnv(process.env.RECORD_SAFER_ALLOW_REMOTE_FILE_TOKEN)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const allowlist = getRemoteFileTokenAllowlist();
|
|
109
|
+
if (allowlist.size === 0) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const parsed = new URL(rawUrl);
|
|
114
|
+
return allowlist.has(parsed.hostname.toLowerCase());
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function loadAuthTokenWithSource() {
|
|
121
|
+
if (process.env.RECORD_SAFER_TOKEN) {
|
|
122
|
+
return { token: process.env.RECORD_SAFER_TOKEN, source: "env" };
|
|
123
|
+
}
|
|
124
|
+
const tokenPath = path.join(os.homedir(), ".record_safer", "auth_token");
|
|
125
|
+
try {
|
|
126
|
+
const token = fs.readFileSync(tokenPath, "utf-8").trim();
|
|
127
|
+
if (token.length > 0) {
|
|
128
|
+
return { token, source: "file" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore
|
|
133
|
+
}
|
|
134
|
+
return { token: null, source: "none" };
|
|
135
|
+
}
|
|
136
|
+
function isSafePortFile(filePath) {
|
|
137
|
+
try {
|
|
138
|
+
const stat = fs.statSync(filePath);
|
|
139
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// 拒绝 group/world 可写,避免被其他用户篡改
|
|
143
|
+
if ((stat.mode & 0o022) !== 0) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function isProcessAlive(pid) {
|
|
153
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
process.kill(pid, 0);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function readPortFromMeta(dir) {
|
|
165
|
+
const metaPath = path.join(dir, "backend_port.meta.json");
|
|
166
|
+
if (!fs.existsSync(metaPath)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (!isSafePortFile(metaPath)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const raw = fs.readFileSync(metaPath, "utf-8");
|
|
174
|
+
const meta = JSON.parse(raw);
|
|
175
|
+
const port = Number(meta?.port);
|
|
176
|
+
const pid = meta?.pid !== undefined ? Number(meta.pid) : null;
|
|
177
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && !isProcessAlive(pid)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return port;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 加载 Auth Token:优先环境变量,其次文件
|
|
191
|
+
* 1. RECORD_SAFER_TOKEN 环境变量
|
|
192
|
+
* 2. ~/.record_safer/auth_token 文件
|
|
193
|
+
*/
|
|
194
|
+
export function loadAuthToken() {
|
|
195
|
+
return loadAuthTokenWithSource().token;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 从端口文件读取后端端口号
|
|
199
|
+
* 返回有效端口号(1-65535)或 null
|
|
200
|
+
*/
|
|
201
|
+
export function readPortFromFile() {
|
|
202
|
+
try {
|
|
203
|
+
const dir = path.join(os.homedir(), ".record_safer");
|
|
204
|
+
const metaPort = readPortFromMeta(dir);
|
|
205
|
+
if (metaPort !== null) {
|
|
206
|
+
return metaPort;
|
|
207
|
+
}
|
|
208
|
+
const portPath = path.join(dir, "backend_port");
|
|
209
|
+
if (!isSafePortFile(portPath)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const content = fs.readFileSync(portPath, "utf-8").trim();
|
|
213
|
+
const port = parseInt(content, 10);
|
|
214
|
+
if (Number.isInteger(port) && port >= 1 && port <= 65535) {
|
|
215
|
+
return port;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 按优先级链解析 baseUrl:
|
|
225
|
+
* 1. 端口文件 → http://127.0.0.1:{port}
|
|
226
|
+
* 2. RECORD_SAFER_URL 环境变量
|
|
227
|
+
* 3. 默认 http://127.0.0.1:8080
|
|
228
|
+
*/
|
|
229
|
+
export function resolveBaseUrl() {
|
|
230
|
+
const port = readPortFromFile();
|
|
231
|
+
if (port !== null) {
|
|
232
|
+
return `http://127.0.0.1:${port}`;
|
|
233
|
+
}
|
|
234
|
+
return process.env.RECORD_SAFER_URL ?? "http://127.0.0.1:8080";
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 启动 Record Safer Pro 应用并等待后端就绪
|
|
238
|
+
* 仅在 macOS 上有效,使用 open -a 命令启动 Electron 应用
|
|
239
|
+
* 启动后轮询 /api/system/info 直到后端响应或超时
|
|
240
|
+
*/
|
|
241
|
+
async function launchAppAndWaitForBackend(getUrl, getAuthHeaders, timeoutMs = 15000) {
|
|
242
|
+
// 启动应用
|
|
243
|
+
try {
|
|
244
|
+
try {
|
|
245
|
+
execSync('open -a "Record Safer Pro"', { timeout: 5000 });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
execSync('open -a "Record Safer"', { timeout: 5000 });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// open 命令失败(应用未安装等),直接返回
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
// 轮询等待后端就绪
|
|
256
|
+
const start = Date.now();
|
|
257
|
+
while (Date.now() - start < timeoutMs) {
|
|
258
|
+
await new Promise(r => setTimeout(r, 500));
|
|
259
|
+
try {
|
|
260
|
+
// 重新解析 URL(端口文件可能在启动后才写入)
|
|
261
|
+
const url = getUrl();
|
|
262
|
+
const headers = getAuthHeaders(url);
|
|
263
|
+
const resp = await fetch(`${url}/api/system/info`, { headers, signal: AbortSignal.timeout(2000) });
|
|
264
|
+
if (resp.ok)
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// 后端还没起来,继续等
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 快速预检后端连接配置(不发起 HTTP 请求)
|
|
275
|
+
* 检查端口文件和 token 是否存在,返回诊断信息
|
|
276
|
+
*/
|
|
277
|
+
export function preflightCheck() {
|
|
278
|
+
const port = readPortFromFile();
|
|
279
|
+
const loadedToken = loadAuthTokenWithSource();
|
|
280
|
+
const token = loadedToken.token;
|
|
281
|
+
const hasPort = port !== null;
|
|
282
|
+
const hasToken = token !== null && token.length > 0;
|
|
283
|
+
const baseUrl = hasPort ? `http://127.0.0.1:${port}` : (process.env.RECORD_SAFER_URL ?? "http://127.0.0.1:8080");
|
|
284
|
+
if (!hasPort && !process.env.RECORD_SAFER_URL) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
baseUrl,
|
|
288
|
+
hasToken,
|
|
289
|
+
hasPort,
|
|
290
|
+
error: `Backend connection failed. Due to the DADAO Engine safety architecture, session initialization and recording start must be performed manually in the Record Safer Pro GUI. Please open Record Safer Pro, complete setup, and enter the recording console before using MCP recording actions.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (!hasToken) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
baseUrl,
|
|
297
|
+
hasToken,
|
|
298
|
+
hasPort,
|
|
299
|
+
error: `Auth token unavailable. Due to the DADAO Engine safety architecture, session initialization and recording start must be performed manually in the Record Safer Pro GUI. Please open Record Safer Pro, complete setup, and enter the recording console before using MCP recording actions.`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (loadedToken.source === "file" &&
|
|
303
|
+
!isLoopbackBaseUrl(baseUrl) &&
|
|
304
|
+
!isRemoteFileTokenAllowed(baseUrl)) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
baseUrl,
|
|
308
|
+
hasToken,
|
|
309
|
+
hasPort,
|
|
310
|
+
error: "Remote backend URL detected. Refusing to send local ~/.record_safer/auth_token to a non-loopback host. Set RECORD_SAFER_TOKEN explicitly for remote use, or set RECORD_SAFER_ALLOW_REMOTE_FILE_TOKEN=1 together with RECORD_SAFER_REMOTE_FILE_TOKEN_HOSTS allowlist.",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, baseUrl, hasToken, hasPort };
|
|
314
|
+
}
|
|
315
|
+
/** Record Safer Backend HTTP 客户端 */
|
|
316
|
+
export class RecordSaferClient {
|
|
317
|
+
explicitBaseUrl;
|
|
318
|
+
authToken;
|
|
319
|
+
authTokenSource;
|
|
320
|
+
remoteFileTokenWarningLogged = false;
|
|
321
|
+
appEnsured = false; // 是否已确认应用在运行
|
|
322
|
+
backendReady = false; // 后端连接是否已验证可用
|
|
323
|
+
constructor(config) {
|
|
324
|
+
this.explicitBaseUrl = config?.baseUrl;
|
|
325
|
+
if (config && Object.prototype.hasOwnProperty.call(config, "authToken")) {
|
|
326
|
+
this.authToken = config.authToken ? config.authToken : null;
|
|
327
|
+
this.authTokenSource = this.authToken ? "config" : "none";
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const loaded = loadAuthTokenWithSource();
|
|
331
|
+
this.authToken = loaded.token;
|
|
332
|
+
this.authTokenSource = loaded.source;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
getBaseUrl() {
|
|
336
|
+
return this.explicitBaseUrl ?? resolveBaseUrl();
|
|
337
|
+
}
|
|
338
|
+
shouldAttachAuthToken(baseUrl) {
|
|
339
|
+
if (!this.authToken || this.authToken.length === 0) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (this.authTokenSource === "config" || this.authTokenSource === "env") {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (this.authTokenSource !== "file") {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (isLoopbackBaseUrl(baseUrl)) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
return isRemoteFileTokenAllowed(baseUrl);
|
|
352
|
+
}
|
|
353
|
+
buildAuthHeaders(baseUrl) {
|
|
354
|
+
if (this.shouldAttachAuthToken(baseUrl) && this.authToken) {
|
|
355
|
+
return { Authorization: `Bearer ${this.authToken}` };
|
|
356
|
+
}
|
|
357
|
+
if (this.authToken &&
|
|
358
|
+
this.authTokenSource === "file" &&
|
|
359
|
+
!isLoopbackBaseUrl(baseUrl) &&
|
|
360
|
+
!this.remoteFileTokenWarningLogged) {
|
|
361
|
+
this.remoteFileTokenWarningLogged = true;
|
|
362
|
+
console.warn("[RecordSaferClient] Blocked sending local auth_token to remote backend. " +
|
|
363
|
+
"Use RECORD_SAFER_TOKEN for remote target, or set RECORD_SAFER_ALLOW_REMOTE_FILE_TOKEN=1 together with RECORD_SAFER_REMOTE_FILE_TOKEN_HOSTS.");
|
|
364
|
+
}
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
toMonitorChannelId(index) {
|
|
368
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
369
|
+
throw new Error(`Invalid channel index: ${index}`);
|
|
370
|
+
}
|
|
371
|
+
return index + 1;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 快速预检:检查端口和 token 配置是否就绪
|
|
375
|
+
* 不发起 HTTP 请求,仅检查本地文件
|
|
376
|
+
* 返回 null 表示配置就绪,否则返回错误消息
|
|
377
|
+
*/
|
|
378
|
+
preflight() {
|
|
379
|
+
if (this.backendReady)
|
|
380
|
+
return null;
|
|
381
|
+
if (this.explicitBaseUrl)
|
|
382
|
+
return null; // 显式配置了 URL,跳过预检
|
|
383
|
+
const result = preflightCheck();
|
|
384
|
+
if (!result.ok) {
|
|
385
|
+
return result.error;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 确保 Record Safer 应用正在运行
|
|
391
|
+
* 首次调用时检查后端是否可达,不可达则自动启动应用并等待就绪
|
|
392
|
+
* @returns true 如果它尝试了启动应用操作,false 如果已经在运行
|
|
393
|
+
*/
|
|
394
|
+
async ensureAppRunning() {
|
|
395
|
+
if (this.appEnsured)
|
|
396
|
+
return false;
|
|
397
|
+
// 先尝试一次快速请求,看后端是否已经在运行
|
|
398
|
+
try {
|
|
399
|
+
const baseUrl = this.getBaseUrl();
|
|
400
|
+
const headers = this.buildAuthHeaders(baseUrl);
|
|
401
|
+
const resp = await fetch(`${baseUrl}/api/system/info`, {
|
|
402
|
+
headers,
|
|
403
|
+
signal: AbortSignal.timeout(2000),
|
|
404
|
+
});
|
|
405
|
+
if (resp.ok) {
|
|
406
|
+
this.appEnsured = true;
|
|
407
|
+
this.backendReady = true;
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// 连接失败,需要启动应用
|
|
413
|
+
}
|
|
414
|
+
// 后端不可达,尝试启动应用
|
|
415
|
+
const launched = await launchAppAndWaitForBackend(() => this.getBaseUrl(), (baseUrl) => this.buildAuthHeaders(baseUrl));
|
|
416
|
+
if (launched) {
|
|
417
|
+
// 启动后重新加载 auth token(应用启动时可能生成新 token)
|
|
418
|
+
const loaded = loadAuthTokenWithSource();
|
|
419
|
+
if (loaded.token) {
|
|
420
|
+
this.authToken = loaded.token;
|
|
421
|
+
this.authTokenSource = loaded.source;
|
|
422
|
+
this.remoteFileTokenWarningLogged = false;
|
|
423
|
+
}
|
|
424
|
+
this.backendReady = true;
|
|
425
|
+
}
|
|
426
|
+
this.appEnsured = true;
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* 通用 HTTP 请求方法
|
|
431
|
+
* - 自动附加 Authorization header(当 token 可用时)
|
|
432
|
+
* - 自动设置 Content-Type: application/json(当有 body 时)
|
|
433
|
+
* - HTTP 错误时抛出 ApiError
|
|
434
|
+
*/
|
|
435
|
+
async request(urlPath, options, requestOptions) {
|
|
436
|
+
if (requestOptions?.allowAutoLaunch !== false) {
|
|
437
|
+
await this.ensureAppRunning();
|
|
438
|
+
}
|
|
439
|
+
const baseUrl = this.getBaseUrl();
|
|
440
|
+
const url = `${baseUrl}${urlPath}`;
|
|
441
|
+
const headers = new Headers(options?.headers);
|
|
442
|
+
const authHeaders = this.buildAuthHeaders(baseUrl);
|
|
443
|
+
if (authHeaders.Authorization) {
|
|
444
|
+
headers.set("Authorization", authHeaders.Authorization);
|
|
445
|
+
}
|
|
446
|
+
if (options?.body) {
|
|
447
|
+
headers.set("Content-Type", "application/json");
|
|
448
|
+
}
|
|
449
|
+
const response = await fetch(url, {
|
|
450
|
+
...options,
|
|
451
|
+
headers,
|
|
452
|
+
});
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
const body = await response.text();
|
|
455
|
+
throw new ApiError(response.status, body);
|
|
456
|
+
}
|
|
457
|
+
return (await response.json());
|
|
458
|
+
}
|
|
459
|
+
// ===== 设备管理 =====
|
|
460
|
+
async listDevices(requestOptions) {
|
|
461
|
+
return this.request("/api/devices", undefined, requestOptions);
|
|
462
|
+
}
|
|
463
|
+
async listOutputDevices() {
|
|
464
|
+
const data = await this.request("/api/devices/outputs");
|
|
465
|
+
return Array.isArray(data?.devices) ? data.devices : [];
|
|
466
|
+
}
|
|
467
|
+
async getBufferSizes(deviceId, sampleRate) {
|
|
468
|
+
const params = new URLSearchParams();
|
|
469
|
+
if (typeof sampleRate === "number" && sampleRate > 0) {
|
|
470
|
+
params.set("sampleRate", String(Math.round(sampleRate)));
|
|
471
|
+
}
|
|
472
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
473
|
+
return this.request(`/api/devices/${deviceId}/buffer-sizes${query}`);
|
|
474
|
+
}
|
|
475
|
+
async getDeviceSignals(deviceId, sampleRate) {
|
|
476
|
+
const params = new URLSearchParams();
|
|
477
|
+
if (typeof sampleRate === "number" && sampleRate > 0) {
|
|
478
|
+
params.set("sampleRate", String(Math.round(sampleRate)));
|
|
479
|
+
}
|
|
480
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
481
|
+
return this.request(`/api/devices/${deviceId}/signals${query}`);
|
|
482
|
+
}
|
|
483
|
+
async getDeviceLevels(deviceId, params) {
|
|
484
|
+
const queryParams = new URLSearchParams();
|
|
485
|
+
if (typeof params?.sampleRate === "number" && params.sampleRate > 0) {
|
|
486
|
+
queryParams.set("sampleRate", String(Math.round(params.sampleRate)));
|
|
487
|
+
}
|
|
488
|
+
if (typeof params?.bufferSize === "number" && params.bufferSize > 0) {
|
|
489
|
+
queryParams.set("bufferSize", String(Math.round(params.bufferSize)));
|
|
490
|
+
}
|
|
491
|
+
const query = queryParams.size > 0 ? `?${queryParams.toString()}` : "";
|
|
492
|
+
return this.request(`/api/devices/${deviceId}/levels${query}`);
|
|
493
|
+
}
|
|
494
|
+
async stopLevelMonitoring() {
|
|
495
|
+
await this.request("/api/devices/levels/stop", { method: "POST" });
|
|
496
|
+
}
|
|
497
|
+
// ===== 电平监控 =====
|
|
498
|
+
async subscribeLevels(params) {
|
|
499
|
+
const body = params && Object.keys(params).length > 0
|
|
500
|
+
? JSON.stringify({
|
|
501
|
+
...(params.deviceId ? { deviceId: parseInt(params.deviceId, 10) || 0 } : {}),
|
|
502
|
+
...(typeof params.channelCount === "number" ? { channelCount: params.channelCount } : {}),
|
|
503
|
+
...(typeof params.sampleRate === "number" ? { sampleRate: params.sampleRate } : {}),
|
|
504
|
+
})
|
|
505
|
+
: undefined;
|
|
506
|
+
await this.request("/api/levels/subscribe", { method: "POST", body });
|
|
507
|
+
}
|
|
508
|
+
async unsubscribeLevels() {
|
|
509
|
+
await this.request("/api/levels/unsubscribe", { method: "POST" });
|
|
510
|
+
}
|
|
511
|
+
async disableLevelCallback() {
|
|
512
|
+
await this.request("/api/levels/disable-callback", { method: "POST" });
|
|
513
|
+
}
|
|
514
|
+
async enableLevelCallback() {
|
|
515
|
+
await this.request("/api/levels/enable-callback", { method: "POST" });
|
|
516
|
+
}
|
|
517
|
+
// ===== 会话与录音 =====
|
|
518
|
+
/**
|
|
519
|
+
* 初始化录音会话
|
|
520
|
+
* 接收 MCP 工具层的简化参数,转换为后端期望的格式后发送
|
|
521
|
+
*/
|
|
522
|
+
async initializeSession(params) {
|
|
523
|
+
// 步骤 1: 先设置输出路径(后端 initialize 不会保存 outputPath,需要单独调用)
|
|
524
|
+
// 这与前端 GUI 的行为一致
|
|
525
|
+
await this.setOutputPath(params.outputPath);
|
|
526
|
+
// 步骤 2: 设置音频格式
|
|
527
|
+
await this.setAudioFormat({
|
|
528
|
+
format: params.audioFormat,
|
|
529
|
+
sampleRate: params.sampleRate,
|
|
530
|
+
bitDepth: params.bitDepth,
|
|
531
|
+
});
|
|
532
|
+
// 步骤 3: 初始化会话
|
|
533
|
+
const backendPayload = {
|
|
534
|
+
device: { id: parseInt(params.deviceId, 10) },
|
|
535
|
+
sampleRate: params.sampleRate,
|
|
536
|
+
bitDepth: params.bitDepth,
|
|
537
|
+
channels: params.channels.map(ch => ({
|
|
538
|
+
id: ch,
|
|
539
|
+
active: true,
|
|
540
|
+
name: `Channel ${ch}`,
|
|
541
|
+
})),
|
|
542
|
+
outputPath: params.outputPath,
|
|
543
|
+
audioFormat: params.audioFormat,
|
|
544
|
+
};
|
|
545
|
+
return this.request("/api/session/initialize", {
|
|
546
|
+
method: "POST",
|
|
547
|
+
body: JSON.stringify(backendPayload),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async startRecordMcp(params) {
|
|
551
|
+
if (params.userConfirmed !== true) {
|
|
552
|
+
throw new Error("start_record_mcp requires explicit user confirmation before execution");
|
|
553
|
+
}
|
|
554
|
+
const usedDefaultBitDepth = !params.bitDepth;
|
|
555
|
+
const bitDepth = params.bitDepth ?? "24-bit";
|
|
556
|
+
const usedDefaultOutputPath = !params.outputPath || params.outputPath.trim().length === 0;
|
|
557
|
+
const outputPath = usedDefaultOutputPath
|
|
558
|
+
? path.join(os.homedir(), "Desktop")
|
|
559
|
+
: params.outputPath.trim();
|
|
560
|
+
const deviceId = parseInt(params.deviceId, 10);
|
|
561
|
+
if (!Number.isInteger(deviceId) || deviceId <= 0) {
|
|
562
|
+
throw new Error(`Invalid deviceId: ${params.deviceId}`);
|
|
563
|
+
}
|
|
564
|
+
const devices = await this.listDevices({ allowAutoLaunch: false });
|
|
565
|
+
const deviceInfo = devices.find((device) => device.id === params.deviceId);
|
|
566
|
+
if (!deviceInfo) {
|
|
567
|
+
throw new Error(`Input device ${params.deviceId} is not available`);
|
|
568
|
+
}
|
|
569
|
+
const deviceDefaultSampleRate = Number.isInteger(deviceInfo.defaultSampleRate) && deviceInfo.defaultSampleRate > 0
|
|
570
|
+
? deviceInfo.defaultSampleRate
|
|
571
|
+
: 48000;
|
|
572
|
+
const requestedSampleRate = Number.isInteger(params.sampleRate) && params.sampleRate > 0 ? params.sampleRate : null;
|
|
573
|
+
if (requestedSampleRate !== null && requestedSampleRate !== deviceDefaultSampleRate) {
|
|
574
|
+
throw new Error(`Requested sample rate ${requestedSampleRate} Hz does not match the selected device default/current rate ${deviceDefaultSampleRate} Hz. Current MCP preparation cannot switch hardware sample rate; confirm the device rate or adjust it in the app first.`);
|
|
575
|
+
}
|
|
576
|
+
const sampleRate = requestedSampleRate ?? deviceDefaultSampleRate;
|
|
577
|
+
const usedDeviceDefaultSampleRate = requestedSampleRate === null;
|
|
578
|
+
const channelPayload = params.channels.map((channel) => ({
|
|
579
|
+
id: channel.id,
|
|
580
|
+
active: channel.armed !== false,
|
|
581
|
+
name: `Channel ${channel.id}`,
|
|
582
|
+
}));
|
|
583
|
+
const requestedChannelRenames = params.channels
|
|
584
|
+
.map((channel) => ({
|
|
585
|
+
id: channel.id,
|
|
586
|
+
name: channel.name?.trim() ?? "",
|
|
587
|
+
}))
|
|
588
|
+
.filter((channel) => channel.name.length > 0);
|
|
589
|
+
const activeChannelCount = channelPayload.filter((channel) => channel.active).length;
|
|
590
|
+
if (channelPayload.length === 0 || activeChannelCount === 0) {
|
|
591
|
+
throw new Error("start_record_mcp requires at least one armed channel");
|
|
592
|
+
}
|
|
593
|
+
const previousUiState = await this.getUIState().catch(() => null);
|
|
594
|
+
let sessionInitialized = false;
|
|
595
|
+
try {
|
|
596
|
+
await this.setOutputPath(outputPath);
|
|
597
|
+
await this.setAudioFormat({
|
|
598
|
+
format: params.audioFormat,
|
|
599
|
+
sampleRate,
|
|
600
|
+
bitDepth,
|
|
601
|
+
});
|
|
602
|
+
await this.setUIState("RECORDING", 7);
|
|
603
|
+
const payload = {
|
|
604
|
+
device: { id: deviceId },
|
|
605
|
+
sampleRate,
|
|
606
|
+
bitDepth,
|
|
607
|
+
channels: channelPayload,
|
|
608
|
+
outputPath,
|
|
609
|
+
audioFormat: params.audioFormat,
|
|
610
|
+
};
|
|
611
|
+
if (params.sessionName && params.sessionName.trim().length > 0) {
|
|
612
|
+
payload.sessionName = params.sessionName.trim();
|
|
613
|
+
}
|
|
614
|
+
if (typeof params.bufferSize === "number" && params.bufferSize > 0) {
|
|
615
|
+
payload.bufferSize = params.bufferSize;
|
|
616
|
+
}
|
|
617
|
+
await this.request("/api/session/initialize", {
|
|
618
|
+
method: "POST",
|
|
619
|
+
body: JSON.stringify(payload),
|
|
620
|
+
});
|
|
621
|
+
sessionInitialized = true;
|
|
622
|
+
for (const channel of requestedChannelRenames) {
|
|
623
|
+
await this.renameChannel(String(channel.id), channel.name);
|
|
624
|
+
}
|
|
625
|
+
const [status, uiState] = await Promise.all([
|
|
626
|
+
this.getRecordingStatus(),
|
|
627
|
+
this.getUIState(),
|
|
628
|
+
]);
|
|
629
|
+
const sessionConfig = status.sessionConfig;
|
|
630
|
+
const fallbackChannels = channelPayload;
|
|
631
|
+
return {
|
|
632
|
+
success: true,
|
|
633
|
+
page: "RECORDING",
|
|
634
|
+
pageStep: uiState.pageStep,
|
|
635
|
+
consoleReady: uiState.page === "RECORDING" &&
|
|
636
|
+
Array.isArray(sessionConfig?.channels) &&
|
|
637
|
+
sessionConfig.channels.length > 0,
|
|
638
|
+
usedDefaultOutputPath,
|
|
639
|
+
usedDeviceDefaultSampleRate,
|
|
640
|
+
usedDefaultBitDepth,
|
|
641
|
+
appliedChannelRenames: requestedChannelRenames.length,
|
|
642
|
+
outputPath: normalizeDisplayPath(sessionConfig?.outputPath || outputPath),
|
|
643
|
+
deviceId: String(sessionConfig?.deviceId ?? params.deviceId),
|
|
644
|
+
requestedSampleRate,
|
|
645
|
+
requestedBitDepth: bitDepth,
|
|
646
|
+
effectiveSampleRate: sessionConfig?.sampleRate ?? sampleRate,
|
|
647
|
+
effectiveBitDepth: sessionConfig?.bitDepth ?? bitDepth,
|
|
648
|
+
audioFormat: sessionConfig?.audioFormat ?? params.audioFormat,
|
|
649
|
+
sessionName: sessionConfig?.sessionName ?? params.sessionName ?? null,
|
|
650
|
+
channels: (sessionConfig?.channels ?? fallbackChannels).map((channel) => ({
|
|
651
|
+
id: channel.id,
|
|
652
|
+
name: channel.name,
|
|
653
|
+
armed: channel.armed !== undefined ? channel.armed : channel.active !== false,
|
|
654
|
+
deviceChannelLabel: channel.deviceChannelLabel,
|
|
655
|
+
color: channel.color,
|
|
656
|
+
})),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
if (!sessionInitialized && previousUiState != null) {
|
|
661
|
+
await this.setUIState(previousUiState.page, previousUiState.pageStep).catch(() => { });
|
|
662
|
+
}
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async resetSession() {
|
|
667
|
+
await this.request("/api/session/reset", { method: "POST" });
|
|
668
|
+
}
|
|
669
|
+
async startRecording(options) {
|
|
670
|
+
return this.request("/api/recording/start", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
body: options ? JSON.stringify(options) : undefined,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async stopRecording(params) {
|
|
676
|
+
if (params.userConfirmed !== true) {
|
|
677
|
+
throw new Error("stop_recording requires explicit user confirmation before execution");
|
|
678
|
+
}
|
|
679
|
+
return this.request("/api/recording/stop", { method: "POST" });
|
|
680
|
+
}
|
|
681
|
+
async getRecordingStatus(requestOptions) {
|
|
682
|
+
return this.request("/api/recording/status", undefined, requestOptions);
|
|
683
|
+
}
|
|
684
|
+
// ===== 配置 =====
|
|
685
|
+
async getOutputPath() {
|
|
686
|
+
return this.request("/api/config/output-path");
|
|
687
|
+
}
|
|
688
|
+
async setOutputPath(outputPath) {
|
|
689
|
+
await this.request("/api/config/output-path", {
|
|
690
|
+
method: "POST",
|
|
691
|
+
body: JSON.stringify({ path: outputPath }),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async getAudioFormat() {
|
|
695
|
+
return this.request("/api/config/format");
|
|
696
|
+
}
|
|
697
|
+
async setAudioFormat(format) {
|
|
698
|
+
await this.request("/api/config/format", {
|
|
699
|
+
method: "POST",
|
|
700
|
+
body: JSON.stringify(format),
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
async getSettings() {
|
|
704
|
+
const data = await this.request("/api/settings");
|
|
705
|
+
if (!data?.settings || typeof data.settings !== "object") {
|
|
706
|
+
throw new Error("Backend returned an invalid settings payload");
|
|
707
|
+
}
|
|
708
|
+
return data.settings;
|
|
709
|
+
}
|
|
710
|
+
async updateSettings(settings) {
|
|
711
|
+
const currentSettings = await this.getSettings();
|
|
712
|
+
const mergedSettings = {
|
|
713
|
+
...currentSettings,
|
|
714
|
+
...settings,
|
|
715
|
+
};
|
|
716
|
+
if (mergedSettings.audioFormat !== "wav" && mergedSettings.audioFormat !== "caf") {
|
|
717
|
+
throw new Error("Settings update requires a valid audioFormat value");
|
|
718
|
+
}
|
|
719
|
+
if (typeof mergedSettings.showLevelMeterDuringRecording !== "boolean") {
|
|
720
|
+
throw new Error("Settings update requires showLevelMeterDuringRecording to be a boolean");
|
|
721
|
+
}
|
|
722
|
+
await this.request("/api/settings", {
|
|
723
|
+
method: "POST",
|
|
724
|
+
body: JSON.stringify(mergedSettings),
|
|
725
|
+
});
|
|
726
|
+
return mergedSettings;
|
|
727
|
+
}
|
|
728
|
+
// ===== 监听/DSP =====
|
|
729
|
+
async getMonitorStatus() {
|
|
730
|
+
const data = await this.request("/api/monitor/status");
|
|
731
|
+
return data.status;
|
|
732
|
+
}
|
|
733
|
+
async startMonitor(params) {
|
|
734
|
+
const inputDeviceId = parseInt(params.inputDeviceId, 10) || 0;
|
|
735
|
+
const outputDeviceId = parseInt(params.outputDeviceId, 10) || 0;
|
|
736
|
+
await this.request("/api/monitor/start", {
|
|
737
|
+
method: "POST",
|
|
738
|
+
body: JSON.stringify({
|
|
739
|
+
inputDeviceId,
|
|
740
|
+
outputDeviceId,
|
|
741
|
+
...(typeof params.outputLeftChannel === "number" ? { outputLeftChannel: params.outputLeftChannel } : {}),
|
|
742
|
+
...(typeof params.outputRightChannel === "number" ? { outputRightChannel: params.outputRightChannel } : {}),
|
|
743
|
+
...(typeof params.bufferSize === "number" ? { bufferSize: params.bufferSize } : {}),
|
|
744
|
+
}),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
async stopMonitor() {
|
|
748
|
+
await this.request("/api/monitor/stop", { method: "POST" });
|
|
749
|
+
}
|
|
750
|
+
async setMasterVolume(volume, pan) {
|
|
751
|
+
await this.request("/api/monitor/master", {
|
|
752
|
+
method: "POST",
|
|
753
|
+
body: JSON.stringify(pan !== undefined ? { volume, pan } : { volume }),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
async getMixLevels() {
|
|
757
|
+
return this.request("/api/monitor/mix-levels");
|
|
758
|
+
}
|
|
759
|
+
async setChannelVolume(index, volume) {
|
|
760
|
+
const channelId = this.toMonitorChannelId(index);
|
|
761
|
+
await this.request(`/api/monitor/channel/${channelId}/volume`, {
|
|
762
|
+
method: "POST",
|
|
763
|
+
body: JSON.stringify({ volume }),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
async setChannelPan(index, pan) {
|
|
767
|
+
const channelId = this.toMonitorChannelId(index);
|
|
768
|
+
await this.request(`/api/monitor/channel/${channelId}/pan`, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
body: JSON.stringify({ pan }),
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
async setChannelSolo(index, solo) {
|
|
774
|
+
const channelId = this.toMonitorChannelId(index);
|
|
775
|
+
await this.request(`/api/monitor/channel/${channelId}/solo`, {
|
|
776
|
+
method: "POST",
|
|
777
|
+
body: JSON.stringify({ solo }),
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async setChannelMute(index, mute) {
|
|
781
|
+
const channelId = this.toMonitorChannelId(index);
|
|
782
|
+
await this.request(`/api/monitor/channel/${channelId}/mute`, {
|
|
783
|
+
method: "POST",
|
|
784
|
+
body: JSON.stringify({ mute }),
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async setChannelMsProcess(index, enabled) {
|
|
788
|
+
const channelId = this.toMonitorChannelId(index);
|
|
789
|
+
await this.request(`/api/monitor/channel/${channelId}/ms-process`, {
|
|
790
|
+
method: "POST",
|
|
791
|
+
body: JSON.stringify({ enabled }),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async setChannelPhaseInvert(index, inverted) {
|
|
795
|
+
const channelId = this.toMonitorChannelId(index);
|
|
796
|
+
await this.request(`/api/monitor/channel/${channelId}/phase-invert`, {
|
|
797
|
+
method: "POST",
|
|
798
|
+
body: JSON.stringify({ inverted }),
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async mergeChannelsStereo(leftIndex, rightIndex) {
|
|
802
|
+
await this.setChannelPan(leftIndex, -1.0);
|
|
803
|
+
await this.setChannelPan(rightIndex, 1.0);
|
|
804
|
+
}
|
|
805
|
+
async splitStereoChannel(leftIndex, rightIndex) {
|
|
806
|
+
await this.setChannelPan(leftIndex, 0.0);
|
|
807
|
+
await this.setChannelPan(rightIndex, 0.0);
|
|
808
|
+
await this.setChannelMsProcess(leftIndex, false);
|
|
809
|
+
}
|
|
810
|
+
// ===== 轨道与标记 =====
|
|
811
|
+
async listTracks(requestOptions) {
|
|
812
|
+
return this.request("/api/tracks/available", undefined, requestOptions);
|
|
813
|
+
}
|
|
814
|
+
async stopTrack(trackId) {
|
|
815
|
+
await this.request(`/api/tracks/${trackId}/stop`, { method: "POST" });
|
|
816
|
+
}
|
|
817
|
+
async listMarkers(requestOptions) {
|
|
818
|
+
const data = await this.request("/api/markers", undefined, requestOptions);
|
|
819
|
+
return data?.markers || [];
|
|
820
|
+
}
|
|
821
|
+
async addMarker(label) {
|
|
822
|
+
const created = await this.request("/api/markers/add", {
|
|
823
|
+
method: "POST",
|
|
824
|
+
});
|
|
825
|
+
if (created.success !== true || !Number.isInteger(created.id) || typeof created.timestamp !== "number") {
|
|
826
|
+
throw new Error("Backend returned an invalid marker payload");
|
|
827
|
+
}
|
|
828
|
+
const markerId = created.id;
|
|
829
|
+
const markerTimestamp = created.timestamp;
|
|
830
|
+
const normalizedLabel = label?.trim();
|
|
831
|
+
if (normalizedLabel) {
|
|
832
|
+
await this.renameMarker(String(markerId), normalizedLabel);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
success: true,
|
|
836
|
+
id: markerId,
|
|
837
|
+
timestamp: markerTimestamp,
|
|
838
|
+
label: normalizedLabel || created.label || `Marker ${markerId}`,
|
|
839
|
+
time: created.time,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async syncMarkers(markers) {
|
|
843
|
+
await this.request("/api/markers/sync", {
|
|
844
|
+
method: "POST",
|
|
845
|
+
body: JSON.stringify({ markers }),
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async renameMarker(id, name) {
|
|
849
|
+
await this.request(`/api/markers/${id}/rename`, {
|
|
850
|
+
method: "POST",
|
|
851
|
+
body: JSON.stringify({ name }),
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
async renameChannel(id, name) {
|
|
855
|
+
await this.request(`/api/channels/${id}/rename`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
body: JSON.stringify({ name }),
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
async setChannelColor(id, color) {
|
|
861
|
+
await this.request(`/api/channels/${id}/color`, {
|
|
862
|
+
method: "POST",
|
|
863
|
+
body: JSON.stringify({ color }),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
// ===== 存储 =====
|
|
867
|
+
async listVolumes() {
|
|
868
|
+
const data = await this.request("/api/storage/volumes");
|
|
869
|
+
return Array.isArray(data?.volumes) ? data.volumes : [];
|
|
870
|
+
}
|
|
871
|
+
async listExternalDrives() {
|
|
872
|
+
const data = await this.request("/api/storage/external");
|
|
873
|
+
return Array.isArray(data?.volumes) ? data.volumes : [];
|
|
874
|
+
}
|
|
875
|
+
async validateStoragePath(storagePath) {
|
|
876
|
+
return this.request("/api/storage/validate", {
|
|
877
|
+
method: "POST",
|
|
878
|
+
body: JSON.stringify({ path: storagePath }),
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async lockStorage(uuid) {
|
|
882
|
+
await this.request("/api/storage/lock", {
|
|
883
|
+
method: "POST",
|
|
884
|
+
body: JSON.stringify({ uuid }),
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async unlockStorage() {
|
|
888
|
+
await this.request("/api/storage/unlock", { method: "POST" });
|
|
889
|
+
}
|
|
890
|
+
// ===== 系统 =====
|
|
891
|
+
async getUIState(requestOptions) {
|
|
892
|
+
return this.request("/api/ui/state", undefined, requestOptions);
|
|
893
|
+
}
|
|
894
|
+
async setUIState(page, pageStep) {
|
|
895
|
+
await this.request("/api/ui/state", {
|
|
896
|
+
method: "POST",
|
|
897
|
+
body: JSON.stringify({ page, pageStep }),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
// ===== MCP 分场景查询 =====
|
|
901
|
+
async getMcpChannels(requestOptions) {
|
|
902
|
+
return this.request("/api/mcp/channels", undefined, requestOptions);
|
|
903
|
+
}
|
|
904
|
+
async getMcpMix(requestOptions) {
|
|
905
|
+
return this.request("/api/mcp/mix", undefined, requestOptions);
|
|
906
|
+
}
|
|
907
|
+
async getMcpHealth(requestOptions) {
|
|
908
|
+
return this.request("/api/mcp/health", undefined, requestOptions);
|
|
909
|
+
}
|
|
910
|
+
async getMcpStatus(requestOptions) {
|
|
911
|
+
return this.request("/api/mcp/status", undefined, requestOptions);
|
|
912
|
+
}
|
|
913
|
+
async getSystemInfo() {
|
|
914
|
+
return this.request("/api/system/info");
|
|
915
|
+
}
|
|
916
|
+
async getDiskSpace() {
|
|
917
|
+
return this.request("/api/system/disk-space");
|
|
918
|
+
}
|
|
919
|
+
async checkDiskHealth(params) {
|
|
920
|
+
const body = params && (params.path || params.deviceId)
|
|
921
|
+
? JSON.stringify({
|
|
922
|
+
...(params.path ? { path: params.path } : {}),
|
|
923
|
+
...(params.deviceId ? { deviceId: parseInt(params.deviceId, 10) || 0 } : {}),
|
|
924
|
+
})
|
|
925
|
+
: undefined;
|
|
926
|
+
return this.request("/api/system/disk-health", { method: "POST", body });
|
|
927
|
+
}
|
|
928
|
+
async checkUpdates() {
|
|
929
|
+
return this.request("/api/system/update");
|
|
930
|
+
}
|
|
931
|
+
async getUpdateBadge() {
|
|
932
|
+
return this.request("/api/system/update-badge");
|
|
933
|
+
}
|
|
934
|
+
async installUpdate() {
|
|
935
|
+
await this.request("/api/system/install-update", { method: "POST" });
|
|
936
|
+
}
|
|
937
|
+
async getDownloadProgress() {
|
|
938
|
+
return this.request("/api/system/download-progress");
|
|
939
|
+
}
|
|
940
|
+
// ===== 激活 =====
|
|
941
|
+
async activateLicense(code) {
|
|
942
|
+
return this.request("/api/activation/activate", {
|
|
943
|
+
method: "POST",
|
|
944
|
+
body: JSON.stringify({ code }),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async getActivationStatus() {
|
|
948
|
+
return this.request("/api/activation/status");
|
|
949
|
+
}
|
|
950
|
+
async setActivation(data) {
|
|
951
|
+
try {
|
|
952
|
+
await this.request("/api/activation/set", {
|
|
953
|
+
method: "POST",
|
|
954
|
+
body: JSON.stringify(data),
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
if (error instanceof ApiError && error.statusCode === 404) {
|
|
959
|
+
throw new Error("set_activation is only available on debug backends with RECORD_SAFER_ENABLE_DEBUG_ACTIVATION_API=1.");
|
|
960
|
+
}
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async restoreLicense() {
|
|
965
|
+
return this.request("/api/activation/restore", { method: "POST" });
|
|
966
|
+
}
|
|
967
|
+
// ===== 工具 =====
|
|
968
|
+
async startSplit(params) {
|
|
969
|
+
return this.request("/api/tools/split", {
|
|
970
|
+
method: "POST",
|
|
971
|
+
body: JSON.stringify(params),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
async getSplitProgress() {
|
|
975
|
+
return this.request("/api/tools/split/progress");
|
|
976
|
+
}
|
|
977
|
+
async cancelSplit() {
|
|
978
|
+
await this.request("/api/tools/split/cancel", { method: "POST" });
|
|
979
|
+
}
|
|
980
|
+
// ===== 系统控制 =====
|
|
981
|
+
async sendTelemetry(data) {
|
|
982
|
+
await this.request("/api/telemetry/recording-start", {
|
|
983
|
+
method: "POST",
|
|
984
|
+
body: JSON.stringify(data),
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
async shutdown() {
|
|
988
|
+
await this.request("/api/shutdown", { method: "POST" });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
//# sourceMappingURL=api-client.js.map
|