@greatlhd/ailo-desktop 1.0.0 → 1.0.1
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/dist/cli.js +31 -8
- package/dist/config_server.js +14 -162
- package/dist/index.js +207 -173
- package/dist/static/app.css +252 -13
- package/dist/static/app.html +71 -84
- package/dist/static/app.js +326 -108
- package/package.json +3 -9
- package/src/cli.ts +35 -8
- package/src/config_server.ts +22 -171
- package/src/index.ts +221 -177
- package/src/static/app.css +252 -13
- package/src/static/app.html +71 -84
- package/src/static/app.js +326 -108
- package/src/dingtalk-types.ts +0 -26
- package/src/qq-types.ts +0 -49
- package/src/qq-ws.ts +0 -223
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greatlhd/ailo-desktop",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Ailo 超级端点 — 桌面能力 +
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Ailo 超级端点 — 桌面能力 + 飞书,按需配置启用。支持桌面模式和服务器模式。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -14,22 +14,16 @@
|
|
|
14
14
|
"start": "node dist/index.js"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@greatlhd/ailo-endpoint-sdk": "^1.0.
|
|
17
|
+
"@greatlhd/ailo-endpoint-sdk": "^1.0.1",
|
|
18
18
|
"@larksuiteoapi/node-sdk": "^1.56.1",
|
|
19
|
-
"dingtalk-stream": "^2.0.4",
|
|
20
19
|
"glob": "^11.0.0",
|
|
21
|
-
"imapflow": "^1.2.10",
|
|
22
|
-
"mailparser": "^3.6.6",
|
|
23
|
-
"nodemailer": "^8.0.1",
|
|
24
20
|
"pixelmatch": "^7.1.0",
|
|
25
21
|
"playwright": "^1.50.0",
|
|
26
22
|
"pngjs": "^7.0.0",
|
|
27
23
|
"ws": "^8.18.0"
|
|
28
24
|
},
|
|
29
25
|
"devDependencies": {
|
|
30
|
-
"@types/mailparser": "^3.4.5",
|
|
31
26
|
"@types/node": "^22.0.0",
|
|
32
|
-
"@types/nodemailer": "^6.4.17",
|
|
33
27
|
"@types/pixelmatch": "^5.2.0",
|
|
34
28
|
"@types/ws": "^8.5.0",
|
|
35
29
|
"tsx": "^4.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI init command for ailo-desktop.
|
|
3
|
-
* Usage: ailo-desktop init [--defaults]
|
|
3
|
+
* Usage: ailo-desktop init [--defaults] [--config-dir <path>]
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
|
-
import { join } from "path";
|
|
7
|
+
import { join, resolve } from "path";
|
|
8
|
+
import { mkdirSync } from "fs";
|
|
8
9
|
import { writeConfig } from "@greatlhd/ailo-endpoint-sdk";
|
|
9
10
|
import { CONFIG_FILENAME } from "./constants.js";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
function parseCliArgs(): { useDefaults: boolean; configDir: string } {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
let useDefaults = false;
|
|
15
|
+
let configDir = process.cwd();
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
if (args[i] === "--defaults") {
|
|
19
|
+
useDefaults = true;
|
|
20
|
+
} else if ((args[i] === "--config-dir" || args[i] === "-c") && args[i + 1]) {
|
|
21
|
+
configDir = resolve(args[++i]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { useDefaults, configDir };
|
|
26
|
+
}
|
|
12
27
|
|
|
13
28
|
async function prompt(question: string, defaultVal = ""): Promise<string> {
|
|
14
29
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -21,8 +36,16 @@ async function prompt(question: string, defaultVal = ""): Promise<string> {
|
|
|
21
36
|
});
|
|
22
37
|
}
|
|
23
38
|
|
|
24
|
-
export async function runInit(
|
|
39
|
+
export async function runInit(useDefaultsArg?: boolean, configDirArg?: string): Promise<void> {
|
|
40
|
+
const { useDefaults, configDir } = useDefaultsArg !== undefined && configDirArg !== undefined
|
|
41
|
+
? { useDefaults: useDefaultsArg, configDir: configDirArg }
|
|
42
|
+
: parseCliArgs();
|
|
43
|
+
|
|
25
44
|
console.log("=== Ailo Desktop 初始化 ===\n");
|
|
45
|
+
console.log(`配置目录: ${configDir}\n`);
|
|
46
|
+
|
|
47
|
+
mkdirSync(configDir, { recursive: true });
|
|
48
|
+
const configPath = join(configDir, CONFIG_FILENAME);
|
|
26
49
|
|
|
27
50
|
const wsUrl = useDefaults ? "ws://127.0.0.1:19800/ws" : await prompt("Ailo WebSocket URL", "ws://127.0.0.1:19800/ws");
|
|
28
51
|
const apiKey = useDefaults ? "" : await prompt("API Key (留空稍后配置)");
|
|
@@ -36,9 +59,13 @@ export async function runInit(useDefaults = false): Promise<void> {
|
|
|
36
59
|
},
|
|
37
60
|
};
|
|
38
61
|
|
|
39
|
-
writeConfig(
|
|
40
|
-
console.log(`\n已写入 ${
|
|
62
|
+
writeConfig(configPath, config);
|
|
63
|
+
console.log(`\n已写入 ${configPath}`);
|
|
64
|
+
|
|
65
|
+
console.log("\n初始化完成!运行以下命令启动桌面端点:");
|
|
66
|
+
console.log(` ailo-desktop --config-dir ${configDir} --port 3000`);
|
|
67
|
+
}
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
console.
|
|
69
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
70
|
+
runInit().catch(console.error);
|
|
44
71
|
}
|
package/src/config_server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer, type IncomingMessage, type ServerResponse } from "http";
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
|
-
import { join, resolve, dirname
|
|
3
|
+
import { join, resolve, dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
6
|
import { WebSocketServer, type WebSocket } from "ws";
|
|
@@ -45,38 +45,26 @@ interface ConfigServerDeps {
|
|
|
45
45
|
port: number;
|
|
46
46
|
/** config.json path for Ailo connection config */
|
|
47
47
|
configPath?: string;
|
|
48
|
-
/** 蓝图的 URL(与上报给 Ailo 的 blueprints 一致),用于 GET /api/tools 解析内置工具 */
|
|
49
|
-
blueprintUrl?: string;
|
|
50
|
-
/** 远程蓝图 404 时的本地回退路径(绝对路径或相对 cwd) */
|
|
51
|
-
blueprintLocalPath?: string;
|
|
52
48
|
/** 当存在时启用网页聊天:同一端口提供 /chat 与 /chat/ws,并调用 onWebchatReady */
|
|
53
49
|
webchatCtx?: EndpointContext;
|
|
54
50
|
/** 动态获取网页聊天上下文(连接建立后可挂载,无需重启) */
|
|
55
51
|
getWebchatCtx?: () => EndpointContext | null;
|
|
52
|
+
/** 获取端点上下文(用于获取已上报的工具和技能) */
|
|
53
|
+
getEndpointCtx?: () => EndpointContext | null;
|
|
54
|
+
/** 获取已上报的内置工具列表 */
|
|
55
|
+
getEndpointTools?: () => { name: string; description: string }[];
|
|
56
|
+
/** 获取已上报的技能列表 */
|
|
57
|
+
getEndpointSkills?: () => { name: string; description: string }[];
|
|
56
58
|
/** 网页聊天就绪后回调,供 index 的 send 工具使用 */
|
|
57
59
|
onWebchatReady?: (api: { recordAiloReply: (text: string, participantName: string, content?: WebchatContentItem[]) => boolean }) => void;
|
|
58
60
|
/** 请求热重连以刷新服务端 Skills 列表(启用/禁用后调用,无需重启) */
|
|
59
61
|
onRequestReconnect?: () => Promise<void>;
|
|
60
62
|
/** 保存 Ailo 连接配置后调用,用于断线后使用新配置重连 */
|
|
61
63
|
onConnectionConfigSaved?: (config: { ailoWsUrl: string; ailoApiKey: string; endpointId: string }) => Promise<void>;
|
|
62
|
-
/** 邮件配置保存后回调,重建邮件通道 */
|
|
63
|
-
onEmailConfigSaved?: () => Promise<void>;
|
|
64
|
-
/** 获取邮件通道状态 */
|
|
65
|
-
getEmailStatus?: () => { configured: boolean; running: boolean };
|
|
66
64
|
/** 飞书配置保存后回调 */
|
|
67
65
|
onFeishuConfigSaved?: () => Promise<void>;
|
|
68
66
|
/** 获取飞书通道状态 */
|
|
69
67
|
getFeishuStatus?: () => { configured: boolean; running: boolean };
|
|
70
|
-
/** 钉钉配置保存后回调 */
|
|
71
|
-
onDingtalkConfigSaved?: () => Promise<void>;
|
|
72
|
-
/** 获取钉钉通道状态 */
|
|
73
|
-
getDingtalkStatus?: () => { configured: boolean; running: boolean };
|
|
74
|
-
/** QQ 配置保存后回调 */
|
|
75
|
-
onQQConfigSaved?: () => Promise<void>;
|
|
76
|
-
/** 获取 QQ 通道状态 */
|
|
77
|
-
getQQStatus?: () => { configured: boolean; running: boolean };
|
|
78
|
-
/** 动态返回当前所有已激活蓝图的本地路径(用于 /api/blueprints 多蓝图展示) */
|
|
79
|
-
getBlueprintPaths?: () => string[];
|
|
80
68
|
}
|
|
81
69
|
|
|
82
70
|
function getChatHtmlPath(): string {
|
|
@@ -228,33 +216,17 @@ export function startConfigServer(deps: ConfigServerDeps): ConfigServerRef {
|
|
|
228
216
|
if (path === "/api/env/check" && req.method === "GET") return json(res, await getEnvCheck());
|
|
229
217
|
if (path === "/api/env/install" && req.method === "POST") return json(res, await runEnvInstall());
|
|
230
218
|
if (path === "/api/tools" && req.method === "GET") return json(res, await getReportedTools(deps));
|
|
231
|
-
if (path === "/api/
|
|
232
|
-
if (path === "/api/blueprints" && req.method === "GET") return json(res, await getAllBlueprintsInfo(deps));
|
|
219
|
+
if (path === "/api/skills" && req.method === "GET") return json(res, await getReportedSkills(deps));
|
|
233
220
|
// Ailo 连接配置(仅当 configPath 存在时,供桌面端在界面填写并保存)
|
|
234
221
|
if (deps.configPath) {
|
|
235
222
|
if (path === "/api/connection" && req.method === "GET") return json(res, getConnectionConfig(deps.configPath));
|
|
236
223
|
if (path === "/api/connection" && req.method === "POST") return json(res, await saveConnectionConfig(deps.configPath, await body(req), deps.onConnectionConfigSaved));
|
|
237
224
|
}
|
|
238
|
-
// 邮件配置
|
|
239
|
-
if (deps.configPath) {
|
|
240
|
-
if (path === "/api/email/config" && req.method === "GET") return json(res, getEmailConfig(deps.configPath, deps.getEmailStatus));
|
|
241
|
-
if (path === "/api/email/config" && req.method === "POST") return json(res, await saveEmailConfig(deps.configPath, await body(req), deps.onEmailConfigSaved));
|
|
242
|
-
}
|
|
243
225
|
// 飞书配置
|
|
244
226
|
if (deps.configPath) {
|
|
245
227
|
if (path === "/api/feishu/config" && req.method === "GET") return json(res, getPlatformConfig(deps.configPath, "feishu", ["appId", "appSecret"], deps.getFeishuStatus));
|
|
246
228
|
if (path === "/api/feishu/config" && req.method === "POST") return json(res, await savePlatformConfig(deps.configPath, "feishu", ["appId", "appSecret"], await body(req), deps.onFeishuConfigSaved));
|
|
247
229
|
}
|
|
248
|
-
// 钉钉配置
|
|
249
|
-
if (deps.configPath) {
|
|
250
|
-
if (path === "/api/dingtalk/config" && req.method === "GET") return json(res, getPlatformConfig(deps.configPath, "dingtalk", ["clientId", "clientSecret"], deps.getDingtalkStatus));
|
|
251
|
-
if (path === "/api/dingtalk/config" && req.method === "POST") return json(res, await savePlatformConfig(deps.configPath, "dingtalk", ["clientId", "clientSecret"], await body(req), deps.onDingtalkConfigSaved));
|
|
252
|
-
}
|
|
253
|
-
// QQ 配置
|
|
254
|
-
if (deps.configPath) {
|
|
255
|
-
if (path === "/api/qq/config" && req.method === "GET") return json(res, getPlatformConfig(deps.configPath, "qq", ["appId", "appSecret", "apiBase"], deps.getQQStatus));
|
|
256
|
-
if (path === "/api/qq/config" && req.method === "POST") return json(res, await savePlatformConfig(deps.configPath, "qq", ["appId", "appSecret", "apiBase"], await body(req), deps.onQQConfigSaved));
|
|
257
|
-
}
|
|
258
230
|
// MCP
|
|
259
231
|
if (path === "/api/mcp" && req.method === "GET") return json(res, getMCPList(deps.mcpManager));
|
|
260
232
|
if (path === "/api/mcp" && req.method === "POST") return json(res, await deps.mcpManager.handle(JSON.parse(await body(req))));
|
|
@@ -439,109 +411,33 @@ async function runEnvInstall(): Promise<{ installed: string[]; errors: string[]
|
|
|
439
411
|
return { installed, errors };
|
|
440
412
|
}
|
|
441
413
|
|
|
442
|
-
/** 从蓝图正文中解析 tools 列表(仅提取 name、description) */
|
|
443
|
-
function parseBlueprintTools(md: string): { name: string; description: string }[] {
|
|
444
|
-
const tools: { name: string; description: string }[] = [];
|
|
445
|
-
const frontmatterMatch = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
446
|
-
const yaml = frontmatterMatch?.[1] ?? "";
|
|
447
|
-
let inToolsSection = false;
|
|
448
|
-
let current: { name: string; description: string } | null = null;
|
|
449
|
-
for (const line of yaml.split(/\r?\n/)) {
|
|
450
|
-
if (line.trim() === "tools:") {
|
|
451
|
-
inToolsSection = true;
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
if (!inToolsSection) continue;
|
|
455
|
-
const itemMatch = line.match(/^\s+-\s+name:\s*(.+)$/);
|
|
456
|
-
const descMatch = line.match(/^\s+description:\s*(.+)$/);
|
|
457
|
-
if (itemMatch) {
|
|
458
|
-
if (current) tools.push(current);
|
|
459
|
-
current = { name: itemMatch[1].trim(), description: "" };
|
|
460
|
-
} else if (current && descMatch) {
|
|
461
|
-
current.description = descMatch[1].trim();
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (current) tools.push(current);
|
|
465
|
-
return tools;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function fetchBlueprintContent(url: string, localFallbackPath?: string): Promise<string> {
|
|
469
|
-
if (url.startsWith("file://")) {
|
|
470
|
-
const filePath = url.slice(7);
|
|
471
|
-
return readFileSync(filePath, "utf-8");
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
const res = await fetch(url);
|
|
475
|
-
if (!res.ok) throw new Error(`Blueprint fetch failed: ${res.status}`);
|
|
476
|
-
return res.text();
|
|
477
|
-
} catch (e) {
|
|
478
|
-
if (localFallbackPath && existsSync(localFallbackPath)) {
|
|
479
|
-
return readFileSync(localFallbackPath, "utf-8");
|
|
480
|
-
}
|
|
481
|
-
throw e;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
414
|
async function getReportedTools(deps: ConfigServerDeps): Promise<{ name: string; description: string; source: string }[]> {
|
|
486
415
|
const out: { name: string; description: string; source: string }[] = [];
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const md = await fetchBlueprintContent(deps.blueprintUrl, localPath);
|
|
493
|
-
for (const t of parseBlueprintTools(md)) {
|
|
494
|
-
out.push({ name: t.name, description: t.description, source: "builtin" });
|
|
495
|
-
}
|
|
496
|
-
} catch (e) {
|
|
497
|
-
console.error("[config] 解析蓝图工具失败:", e);
|
|
416
|
+
|
|
417
|
+
// 内置工具
|
|
418
|
+
if (deps.getEndpointTools) {
|
|
419
|
+
for (const t of deps.getEndpointTools()) {
|
|
420
|
+
out.push({ name: t.name, description: t.description, source: "builtin" });
|
|
498
421
|
}
|
|
499
422
|
}
|
|
423
|
+
|
|
424
|
+
// MCP 工具
|
|
500
425
|
for (const t of deps.mcpManager.getAllPrivateTools()) {
|
|
501
426
|
out.push({ name: t.name, description: t.description ?? "", source: "mcp" });
|
|
502
427
|
}
|
|
503
428
|
return out;
|
|
504
429
|
}
|
|
505
430
|
|
|
506
|
-
async function
|
|
507
|
-
|
|
508
|
-
try {
|
|
509
|
-
const localPath = deps.blueprintLocalPath ? resolve(process.cwd(), deps.blueprintLocalPath) : undefined;
|
|
510
|
-
const content = await fetchBlueprintContent(deps.blueprintUrl, localPath);
|
|
511
|
-
return { url: deps.blueprintUrl, content };
|
|
512
|
-
} catch {
|
|
513
|
-
return { url: deps.blueprintUrl, content: null };
|
|
514
|
-
}
|
|
515
|
-
}
|
|
431
|
+
async function getReportedSkills(deps: ConfigServerDeps): Promise<{ name: string; description: string; source: string }[]> {
|
|
432
|
+
const out: { name: string; description: string; source: string }[] = [];
|
|
516
433
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// fallback:如果没有传 getBlueprintPaths,至少返回主蓝图
|
|
521
|
-
if (deps.blueprintUrl) {
|
|
522
|
-
try {
|
|
523
|
-
const localPath = deps.blueprintLocalPath ? resolve(process.cwd(), deps.blueprintLocalPath) : undefined;
|
|
524
|
-
const content = await fetchBlueprintContent(deps.blueprintUrl, localPath);
|
|
525
|
-
const name = basename(deps.blueprintLocalPath ?? deps.blueprintUrl, ".blueprint.md");
|
|
526
|
-
return [{ name, path: deps.blueprintUrl, content }];
|
|
527
|
-
} catch {
|
|
528
|
-
return [];
|
|
529
|
-
}
|
|
434
|
+
if (deps.getEndpointSkills) {
|
|
435
|
+
for (const s of deps.getEndpointSkills()) {
|
|
436
|
+
out.push({ name: s.name, description: s.description, source: "builtin" });
|
|
530
437
|
}
|
|
531
|
-
return [];
|
|
532
438
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const filePath = p.startsWith("file://") ? p.slice(7) : p;
|
|
536
|
-
const name = basename(filePath, ".blueprint.md");
|
|
537
|
-
try {
|
|
538
|
-
const content = readFileSync(filePath, "utf-8");
|
|
539
|
-
return { name, path: p, content };
|
|
540
|
-
} catch {
|
|
541
|
-
return { name, path: p, content: null };
|
|
542
|
-
}
|
|
543
|
-
}),
|
|
544
|
-
);
|
|
439
|
+
|
|
440
|
+
return out;
|
|
545
441
|
}
|
|
546
442
|
|
|
547
443
|
function getMCPList(mgr: LocalMCPManager) {
|
|
@@ -635,51 +531,6 @@ async function saveConnectionConfig(
|
|
|
635
531
|
}
|
|
636
532
|
}
|
|
637
533
|
|
|
638
|
-
function getEmailConfig(
|
|
639
|
-
configPath: string,
|
|
640
|
-
getStatus?: () => { configured: boolean; running: boolean },
|
|
641
|
-
): Record<string, unknown> {
|
|
642
|
-
const cfg = readConfig(configPath) as Record<string, unknown>;
|
|
643
|
-
const email = (cfg.email ?? {}) as Record<string, unknown>;
|
|
644
|
-
const status = getStatus?.() ?? { configured: false, running: false };
|
|
645
|
-
return {
|
|
646
|
-
imapHost: email.imapHost ?? "",
|
|
647
|
-
imapUser: email.imapUser ?? "",
|
|
648
|
-
imapPassword: email.imapPassword ?? "",
|
|
649
|
-
imapPort: email.imapPort ?? 993,
|
|
650
|
-
smtpHost: email.smtpHost ?? "",
|
|
651
|
-
smtpPort: email.smtpPort ?? 465,
|
|
652
|
-
smtpUser: email.smtpUser ?? "",
|
|
653
|
-
smtpPassword: email.smtpPassword ?? "",
|
|
654
|
-
...status,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
async function saveEmailConfig(
|
|
659
|
-
configPath: string,
|
|
660
|
-
bodyStr: string,
|
|
661
|
-
onSaved?: () => Promise<void>,
|
|
662
|
-
): Promise<{ ok: boolean; message?: string; error?: string }> {
|
|
663
|
-
try {
|
|
664
|
-
const bodyTrimmed = (bodyStr ?? "").trim();
|
|
665
|
-
if (!bodyTrimmed) return { ok: false, error: "请求体为空" };
|
|
666
|
-
const existing = readConfig(configPath) as Record<string, unknown>;
|
|
667
|
-
const b = JSON.parse(bodyTrimmed) as Record<string, unknown>;
|
|
668
|
-
const emailFields = ["imapHost", "imapUser", "imapPassword", "imapPort", "smtpHost", "smtpPort", "smtpUser", "smtpPassword"];
|
|
669
|
-
for (const field of emailFields) {
|
|
670
|
-
if (b[field] !== undefined) setNestedValue(existing, `email.${field}`, b[field]);
|
|
671
|
-
}
|
|
672
|
-
writeConfig(configPath, existing);
|
|
673
|
-
if (onSaved) {
|
|
674
|
-
await onSaved();
|
|
675
|
-
return { ok: true, message: "已保存,邮件通道正在重启…" };
|
|
676
|
-
}
|
|
677
|
-
return { ok: true, message: "已保存。" };
|
|
678
|
-
} catch (e: unknown) {
|
|
679
|
-
return { ok: false, error: errMsg(e) };
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
534
|
function getPlatformConfig(
|
|
684
535
|
configPath: string,
|
|
685
536
|
platform: string,
|