@greatlhd/ailo-desktop 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.
Files changed (73) hide show
  1. package/copy-static.mjs +11 -0
  2. package/dist/browser_control.js +767 -0
  3. package/dist/browser_snapshot.js +174 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/code_executor.js +95 -0
  6. package/dist/config_server.js +658 -0
  7. package/dist/connection_util.js +14 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/desktop_state_store.js +57 -0
  10. package/dist/desktop_types.js +1 -0
  11. package/dist/desktop_verifier.js +40 -0
  12. package/dist/dingtalk-handler.js +173 -0
  13. package/dist/dingtalk-types.js +1 -0
  14. package/dist/email_handler.js +501 -0
  15. package/dist/exec_tool.js +90 -0
  16. package/dist/feishu-handler.js +620 -0
  17. package/dist/feishu-types.js +8 -0
  18. package/dist/feishu-utils.js +162 -0
  19. package/dist/fs_tools.js +398 -0
  20. package/dist/index.js +433 -0
  21. package/dist/mcp/config-manager.js +64 -0
  22. package/dist/mcp/index.js +3 -0
  23. package/dist/mcp/rpc.js +109 -0
  24. package/dist/mcp/session.js +140 -0
  25. package/dist/mcp_manager.js +253 -0
  26. package/dist/mouse_keyboard.js +516 -0
  27. package/dist/qq-handler.js +153 -0
  28. package/dist/qq-types.js +15 -0
  29. package/dist/qq-ws.js +178 -0
  30. package/dist/screenshot.js +271 -0
  31. package/dist/skills_hub.js +212 -0
  32. package/dist/skills_manager.js +103 -0
  33. package/dist/static/AGENTS.md +25 -0
  34. package/dist/static/app.css +539 -0
  35. package/dist/static/app.html +292 -0
  36. package/dist/static/app.js +380 -0
  37. package/dist/static/chat.html +994 -0
  38. package/dist/time_tool.js +22 -0
  39. package/dist/utils.js +15 -0
  40. package/package.json +38 -0
  41. package/src/browser_control.ts +739 -0
  42. package/src/browser_snapshot.ts +196 -0
  43. package/src/cli.ts +44 -0
  44. package/src/code_executor.ts +101 -0
  45. package/src/config_server.ts +723 -0
  46. package/src/connection_util.ts +23 -0
  47. package/src/constants.ts +2 -0
  48. package/src/desktop_state_store.ts +64 -0
  49. package/src/desktop_types.ts +44 -0
  50. package/src/desktop_verifier.ts +45 -0
  51. package/src/dingtalk-types.ts +26 -0
  52. package/src/exec_tool.ts +93 -0
  53. package/src/feishu-handler.ts +722 -0
  54. package/src/feishu-types.ts +66 -0
  55. package/src/feishu-utils.ts +174 -0
  56. package/src/fs_tools.ts +411 -0
  57. package/src/index.ts +474 -0
  58. package/src/mcp/config-manager.ts +85 -0
  59. package/src/mcp/index.ts +7 -0
  60. package/src/mcp/rpc.ts +131 -0
  61. package/src/mcp/session.ts +182 -0
  62. package/src/mcp_manager.ts +273 -0
  63. package/src/mouse_keyboard.ts +526 -0
  64. package/src/qq-types.ts +49 -0
  65. package/src/qq-ws.ts +223 -0
  66. package/src/screenshot.ts +297 -0
  67. package/src/static/app.css +539 -0
  68. package/src/static/app.html +292 -0
  69. package/src/static/app.js +380 -0
  70. package/src/static/chat.html +994 -0
  71. package/src/time_tool.ts +24 -0
  72. package/src/utils.ts +22 -0
  73. package/tsconfig.json +13 -0
@@ -0,0 +1,658 @@
1
+ import { createServer } from "http";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join, resolve, dirname, basename } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { spawnSync } from "child_process";
6
+ import { WebSocketServer } from "ws";
7
+ import { textPart, readConfig, writeConfig, getNestedValue, setNestedValue } from "@greatlhd/ailo-endpoint-sdk";
8
+ import { errMsg } from "./utils.js";
9
+ function resolveStaticFile(filename) {
10
+ const base = dirname(fileURLToPath(import.meta.url));
11
+ const inStatic = join(base, "static", filename);
12
+ if (existsSync(inStatic))
13
+ return inStatic;
14
+ return join(base, "..", "src", "static", filename);
15
+ }
16
+ function getChatHtmlPath() {
17
+ return resolveStaticFile("chat.html");
18
+ }
19
+ function serveChatPage(res, htmlPath) {
20
+ try {
21
+ const html = readFileSync(htmlPath, "utf-8");
22
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
23
+ res.end(html);
24
+ }
25
+ catch (e) {
26
+ res.writeHead(500);
27
+ res.end("Failed to load chat page: " + errMsg(e));
28
+ }
29
+ }
30
+ export function startConfigServer(deps) {
31
+ const chatHtmlPath = getChatHtmlPath();
32
+ const clientsByParticipant = new Map();
33
+ const participantByClient = new Map();
34
+ const getWebchatCtx = () => deps.getWebchatCtx?.() ?? deps.webchatCtx ?? null;
35
+ const wss = new WebSocketServer({ noServer: true });
36
+ const MAX_PENDING_PER_USER = 50;
37
+ const PENDING_TTL_MS = 5 * 60 * 1000;
38
+ const pendingMessages = new Map();
39
+ function normalizeParticipantName(participantName) {
40
+ return typeof participantName === "string" ? participantName.trim() : "";
41
+ }
42
+ function enqueuePending(routeName, text) {
43
+ let queue = pendingMessages.get(routeName);
44
+ if (!queue) {
45
+ queue = [];
46
+ pendingMessages.set(routeName, queue);
47
+ }
48
+ queue.push({ text, ts: Date.now() });
49
+ if (queue.length > MAX_PENDING_PER_USER)
50
+ queue.splice(0, queue.length - MAX_PENDING_PER_USER);
51
+ }
52
+ function flushPending(routeName, ws) {
53
+ const queue = pendingMessages.get(routeName);
54
+ if (!queue || queue.length === 0)
55
+ return;
56
+ const now = Date.now();
57
+ const valid = queue.filter((m) => now - m.ts < PENDING_TTL_MS);
58
+ pendingMessages.delete(routeName);
59
+ for (const m of valid) {
60
+ if (ws.readyState === 1)
61
+ ws.send(JSON.stringify({ type: "reply", text: m.text }));
62
+ }
63
+ }
64
+ function bindClient(participantName, ws) {
65
+ const previous = participantByClient.get(ws);
66
+ if (previous && previous !== participantName) {
67
+ const previousSet = clientsByParticipant.get(previous);
68
+ previousSet?.delete(ws);
69
+ if (previousSet && previousSet.size === 0)
70
+ clientsByParticipant.delete(previous);
71
+ }
72
+ let group = clientsByParticipant.get(participantName);
73
+ if (!group) {
74
+ group = new Set();
75
+ clientsByParticipant.set(participantName, group);
76
+ }
77
+ group.add(ws);
78
+ participantByClient.set(ws, participantName);
79
+ flushPending(participantName, ws);
80
+ }
81
+ function unbindClient(ws) {
82
+ const name = participantByClient.get(ws);
83
+ if (!name)
84
+ return;
85
+ participantByClient.delete(ws);
86
+ const group = clientsByParticipant.get(name);
87
+ group?.delete(ws);
88
+ if (group && group.size === 0)
89
+ clientsByParticipant.delete(name);
90
+ }
91
+ function handleRegister(participantName, ws) {
92
+ const routeName = normalizeParticipantName(participantName);
93
+ if (!routeName)
94
+ return;
95
+ bindClient(routeName, ws);
96
+ }
97
+ function handleChatMessage(text, participantName, ws) {
98
+ const ctx = getWebchatCtx();
99
+ if (!ctx || !text?.trim())
100
+ return;
101
+ const routeName = normalizeParticipantName(participantName);
102
+ if (!routeName) {
103
+ ctx.log("warn", "Webchat 上行消息缺少 participantName,已拒绝");
104
+ return;
105
+ }
106
+ bindClient(routeName, ws);
107
+ const tags = [
108
+ { kind: "channel", value: "web", groupWith: true },
109
+ { kind: "conv_type", value: "私聊", groupWith: false },
110
+ { kind: "chat_id", value: routeName, groupWith: true, passToTool: true },
111
+ ];
112
+ const msg = { content: [textPart(text)], contextTags: tags };
113
+ ctx.accept(msg).catch((err) => {
114
+ getWebchatCtx()?.log("error", `Failed to send message to Ailo: ${err instanceof Error ? err.message : err}`);
115
+ });
116
+ }
117
+ function recordAiloReply(text, participantName) {
118
+ const routeName = normalizeParticipantName(participantName);
119
+ if (!routeName)
120
+ return false;
121
+ const group = clientsByParticipant.get(routeName);
122
+ if (!group || group.size === 0) {
123
+ enqueuePending(routeName, text);
124
+ return true;
125
+ }
126
+ const payload = JSON.stringify({ type: "reply", text });
127
+ let sent = 0;
128
+ for (const client of group) {
129
+ if (client.readyState === 1 /* OPEN */) {
130
+ client.send(payload);
131
+ sent += 1;
132
+ }
133
+ }
134
+ if (sent === 0) {
135
+ enqueuePending(routeName, text);
136
+ return true;
137
+ }
138
+ return true;
139
+ }
140
+ const server = createServer(async (req, res) => {
141
+ const url = new URL(req.url ?? "/", `http://localhost:${deps.port}`);
142
+ const path = url.pathname;
143
+ res.setHeader("Access-Control-Allow-Origin", "*");
144
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
145
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
146
+ if (req.method === "OPTIONS") {
147
+ res.writeHead(204);
148
+ res.end();
149
+ return;
150
+ }
151
+ try {
152
+ if (path === "/" || path === "/index.html")
153
+ return serveUI(res, deps);
154
+ if (path === "/app.css" || path === "/app.js")
155
+ return serveStatic(res, path.slice(1));
156
+ if (path === "/chat" && req.method === "GET")
157
+ return serveChatPage(res, chatHtmlPath);
158
+ // Status
159
+ if (path === "/api/status")
160
+ return json(res, deps.getConnectionStatus());
161
+ // 运行环境检测与一键安装(供 Skills、代码执行、浏览器等使用)
162
+ if (path === "/api/env/check" && req.method === "GET")
163
+ return json(res, await getEnvCheck());
164
+ if (path === "/api/env/install" && req.method === "POST")
165
+ return json(res, await runEnvInstall());
166
+ if (path === "/api/tools" && req.method === "GET")
167
+ return json(res, await getReportedTools(deps));
168
+ if (path === "/api/blueprint" && req.method === "GET")
169
+ return json(res, await getBlueprintInfo(deps));
170
+ if (path === "/api/blueprints" && req.method === "GET")
171
+ return json(res, await getAllBlueprintsInfo(deps));
172
+ // Ailo 连接配置(仅当 configPath 存在时,供桌面端在界面填写并保存)
173
+ if (deps.configPath) {
174
+ if (path === "/api/connection" && req.method === "GET")
175
+ return json(res, getConnectionConfig(deps.configPath));
176
+ if (path === "/api/connection" && req.method === "POST")
177
+ return json(res, await saveConnectionConfig(deps.configPath, await body(req), deps.onConnectionConfigSaved));
178
+ }
179
+ // 邮件配置
180
+ if (deps.configPath) {
181
+ if (path === "/api/email/config" && req.method === "GET")
182
+ return json(res, getEmailConfig(deps.configPath, deps.getEmailStatus));
183
+ if (path === "/api/email/config" && req.method === "POST")
184
+ return json(res, await saveEmailConfig(deps.configPath, await body(req), deps.onEmailConfigSaved));
185
+ }
186
+ // 飞书配置
187
+ if (deps.configPath) {
188
+ if (path === "/api/feishu/config" && req.method === "GET")
189
+ return json(res, getPlatformConfig(deps.configPath, "feishu", ["appId", "appSecret"], deps.getFeishuStatus));
190
+ if (path === "/api/feishu/config" && req.method === "POST")
191
+ return json(res, await savePlatformConfig(deps.configPath, "feishu", ["appId", "appSecret"], await body(req), deps.onFeishuConfigSaved));
192
+ }
193
+ // 钉钉配置
194
+ if (deps.configPath) {
195
+ if (path === "/api/dingtalk/config" && req.method === "GET")
196
+ return json(res, getPlatformConfig(deps.configPath, "dingtalk", ["clientId", "clientSecret"], deps.getDingtalkStatus));
197
+ if (path === "/api/dingtalk/config" && req.method === "POST")
198
+ return json(res, await savePlatformConfig(deps.configPath, "dingtalk", ["clientId", "clientSecret"], await body(req), deps.onDingtalkConfigSaved));
199
+ }
200
+ // QQ 配置
201
+ if (deps.configPath) {
202
+ if (path === "/api/qq/config" && req.method === "GET")
203
+ return json(res, getPlatformConfig(deps.configPath, "qq", ["appId", "appSecret", "apiBase"], deps.getQQStatus));
204
+ if (path === "/api/qq/config" && req.method === "POST")
205
+ return json(res, await savePlatformConfig(deps.configPath, "qq", ["appId", "appSecret", "apiBase"], await body(req), deps.onQQConfigSaved));
206
+ }
207
+ // MCP
208
+ if (path === "/api/mcp" && req.method === "GET")
209
+ return json(res, getMCPList(deps.mcpManager));
210
+ if (path === "/api/mcp" && req.method === "POST")
211
+ return json(res, await deps.mcpManager.handle(JSON.parse(await body(req))));
212
+ res.writeHead(404);
213
+ res.end("Not Found");
214
+ }
215
+ catch (e) {
216
+ res.writeHead(500);
217
+ res.end(JSON.stringify({ error: errMsg(e) }));
218
+ }
219
+ });
220
+ wss.on("connection", (ws) => {
221
+ ws.on("message", (data) => {
222
+ try {
223
+ const msg = JSON.parse(Buffer.isBuffer(data) ? data.toString("utf-8") : String(data));
224
+ if (msg.type === "register")
225
+ handleRegister(msg.participantName, ws);
226
+ else if (msg.type === "chat")
227
+ handleChatMessage(msg.text, msg.participantName, ws);
228
+ }
229
+ catch {
230
+ getWebchatCtx()?.log("warn", "Failed to parse WebSocket message");
231
+ }
232
+ });
233
+ ws.on("close", () => unbindClient(ws));
234
+ ws.on("error", () => unbindClient(ws));
235
+ });
236
+ server.on("upgrade", (req, socket, head) => {
237
+ const url = new URL(req.url ?? "/", `http://localhost:${deps.port}`);
238
+ if (url.pathname !== "/chat/ws") {
239
+ socket.destroy();
240
+ return;
241
+ }
242
+ wss.handleUpgrade(req, socket, head, (ws) => {
243
+ wss.emit("connection", ws, req);
244
+ });
245
+ });
246
+ const ref = {
247
+ notifyContextAttached() {
248
+ if (deps.onWebchatReady)
249
+ deps.onWebchatReady({ recordAiloReply });
250
+ },
251
+ };
252
+ server.listen(deps.port, "127.0.0.1", () => {
253
+ console.log(`[config] 配置界面: http://127.0.0.1:${deps.port}`);
254
+ if (deps.onWebchatReady && getWebchatCtx())
255
+ deps.onWebchatReady({ recordAiloReply });
256
+ });
257
+ server.on("error", (err) => {
258
+ const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
259
+ if (code === "EADDRINUSE")
260
+ console.log(`[config] 端口 ${deps.port} 已被占用,跳过配置界面`);
261
+ else
262
+ console.error("[config] 启动失败:", err instanceof Error ? err.message : err);
263
+ });
264
+ return ref;
265
+ }
266
+ function json(res, data) {
267
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
268
+ res.end(JSON.stringify(data, null, 2));
269
+ }
270
+ const MAX_BODY_BYTES = 1024 * 1024; // 1MB
271
+ async function body(req) {
272
+ const chunks = [];
273
+ let total = 0;
274
+ for await (const chunk of req) {
275
+ const buf = chunk;
276
+ total += buf.length;
277
+ if (total > MAX_BODY_BYTES)
278
+ throw new Error("Request body too large");
279
+ chunks.push(buf);
280
+ }
281
+ return Buffer.concat(chunks).toString("utf-8");
282
+ }
283
+ /** 检测 Node(当前进程即 Node,仅取版本) */
284
+ function checkNode() {
285
+ const v = process.version || "";
286
+ return {
287
+ id: "node",
288
+ name: "Node.js",
289
+ description: "JavaScript 代码执行、MCP、桌面端运行环境",
290
+ ok: true,
291
+ detail: v,
292
+ canAutoInstall: false,
293
+ };
294
+ }
295
+ /** 检测 Python:优先 python3,其次 python(Windows 上可能是 python) */
296
+ function checkPython() {
297
+ const commands = process.platform === "win32" ? ["python", "python3", "py"] : ["python3", "python"];
298
+ for (const cmd of commands) {
299
+ const r = spawnSync(cmd, ["--version"], { encoding: "utf-8", timeout: 5000 });
300
+ const out = (r.stdout || r.stderr || "").trim();
301
+ if (r.status === 0 && out) {
302
+ const version = out.replace(/^Python\s+/i, "").split(/\s/)[0]?.trim() || out;
303
+ return {
304
+ id: "python",
305
+ name: "Python",
306
+ description: "代码执行、电子表格等 Skills",
307
+ ok: true,
308
+ detail: version,
309
+ canAutoInstall: false,
310
+ };
311
+ }
312
+ }
313
+ const platform = process.platform;
314
+ let hint;
315
+ if (platform === "win32") {
316
+ hint =
317
+ "1. 从官网下载安装包:https://www.python.org/downloads/\n" +
318
+ "2. 安装时务必勾选「Add Python to PATH」\n" +
319
+ "3. 或使用包管理器:winget install Python.Python.3.12\n" +
320
+ "4. 安装后重启终端,执行 python --version 或 py -3 --version 验证";
321
+ }
322
+ else if (platform === "darwin") {
323
+ hint =
324
+ "1. 推荐使用 Homebrew:brew install python\n" +
325
+ "2. 或从官网下载:https://www.python.org/downloads/\n" +
326
+ "3. 安装后执行 python3 --version 验证";
327
+ }
328
+ else {
329
+ hint =
330
+ "1. Debian/Ubuntu:sudo apt update && sudo apt install python3 python3-pip\n" +
331
+ "2. Fedora/RHEL:sudo dnf install python3 python3-pip\n" +
332
+ "3. 或从官网下载:https://www.python.org/downloads/\n" +
333
+ "4. 安装后执行 python3 --version 验证";
334
+ }
335
+ return {
336
+ id: "python",
337
+ name: "Python",
338
+ description: "代码执行、电子表格等 Skills",
339
+ ok: false,
340
+ hint,
341
+ canAutoInstall: false,
342
+ };
343
+ }
344
+ /** 检测 Playwright Chromium 是否已安装 */
345
+ async function checkPlaywright() {
346
+ try {
347
+ const playwright = await import("playwright");
348
+ const exe = playwright.chromium.executablePath();
349
+ if (existsSync(exe)) {
350
+ return {
351
+ id: "playwright",
352
+ name: "Playwright Chromium",
353
+ description: "浏览器自动化、可见窗口操作、网页抓取等",
354
+ ok: true,
355
+ detail: "已安装",
356
+ canAutoInstall: true,
357
+ };
358
+ }
359
+ }
360
+ catch { }
361
+ return {
362
+ id: "playwright",
363
+ name: "Playwright Chromium",
364
+ description: "浏览器自动化、可见窗口操作、网页抓取等",
365
+ ok: false,
366
+ hint: "本依赖支持一键安装。\n\n" +
367
+ "点击本页下方「安装缺失依赖」按钮,将自动执行 npx playwright install chromium 下载 Chromium 浏览器。\n\n" +
368
+ "若需手动安装:在项目目录或任意目录执行 npx playwright install chromium。",
369
+ canAutoInstall: true,
370
+ };
371
+ }
372
+ async function getEnvCheck() {
373
+ const playwright = await checkPlaywright();
374
+ const runtimes = [
375
+ checkNode(),
376
+ checkPython(),
377
+ playwright,
378
+ ];
379
+ return { runtimes };
380
+ }
381
+ /** 安装可自动安装的依赖(目前仅 Playwright Chromium) */
382
+ async function runEnvInstall() {
383
+ const { execSync } = await import("child_process");
384
+ const installed = [];
385
+ const errors = [];
386
+ try {
387
+ execSync("npx playwright install chromium", { stdio: "pipe", timeout: 120000, encoding: "utf-8" });
388
+ installed.push("Playwright Chromium");
389
+ }
390
+ catch (e) {
391
+ errors.push(`Playwright: ${errMsg(e)}`);
392
+ }
393
+ return { installed, errors };
394
+ }
395
+ /** 从蓝图正文中解析 tools 列表(仅提取 name、description) */
396
+ function parseBlueprintTools(md) {
397
+ const tools = [];
398
+ const frontmatterMatch = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
399
+ const yaml = frontmatterMatch?.[1] ?? "";
400
+ let inToolsSection = false;
401
+ let current = null;
402
+ for (const line of yaml.split(/\r?\n/)) {
403
+ if (line.trim() === "tools:") {
404
+ inToolsSection = true;
405
+ continue;
406
+ }
407
+ if (!inToolsSection)
408
+ continue;
409
+ const itemMatch = line.match(/^\s+-\s+name:\s*(.+)$/);
410
+ const descMatch = line.match(/^\s+description:\s*(.+)$/);
411
+ if (itemMatch) {
412
+ if (current)
413
+ tools.push(current);
414
+ current = { name: itemMatch[1].trim(), description: "" };
415
+ }
416
+ else if (current && descMatch) {
417
+ current.description = descMatch[1].trim();
418
+ }
419
+ }
420
+ if (current)
421
+ tools.push(current);
422
+ return tools;
423
+ }
424
+ async function fetchBlueprintContent(url, localFallbackPath) {
425
+ if (url.startsWith("file://")) {
426
+ const filePath = url.slice(7);
427
+ return readFileSync(filePath, "utf-8");
428
+ }
429
+ try {
430
+ const res = await fetch(url);
431
+ if (!res.ok)
432
+ throw new Error(`Blueprint fetch failed: ${res.status}`);
433
+ return res.text();
434
+ }
435
+ catch (e) {
436
+ if (localFallbackPath && existsSync(localFallbackPath)) {
437
+ return readFileSync(localFallbackPath, "utf-8");
438
+ }
439
+ throw e;
440
+ }
441
+ }
442
+ async function getReportedTools(deps) {
443
+ const out = [];
444
+ if (deps.blueprintUrl) {
445
+ try {
446
+ const localPath = deps.blueprintLocalPath
447
+ ? resolve(process.cwd(), deps.blueprintLocalPath)
448
+ : undefined;
449
+ const md = await fetchBlueprintContent(deps.blueprintUrl, localPath);
450
+ for (const t of parseBlueprintTools(md)) {
451
+ out.push({ name: t.name, description: t.description, source: "builtin" });
452
+ }
453
+ }
454
+ catch (e) {
455
+ console.error("[config] 解析蓝图工具失败:", e);
456
+ }
457
+ }
458
+ for (const t of deps.mcpManager.getAllPrivateTools()) {
459
+ out.push({ name: t.name, description: t.description ?? "", source: "mcp" });
460
+ }
461
+ return out;
462
+ }
463
+ async function getBlueprintInfo(deps) {
464
+ if (!deps.blueprintUrl)
465
+ return { url: null, content: null };
466
+ try {
467
+ const localPath = deps.blueprintLocalPath ? resolve(process.cwd(), deps.blueprintLocalPath) : undefined;
468
+ const content = await fetchBlueprintContent(deps.blueprintUrl, localPath);
469
+ return { url: deps.blueprintUrl, content };
470
+ }
471
+ catch {
472
+ return { url: deps.blueprintUrl, content: null };
473
+ }
474
+ }
475
+ async function getAllBlueprintsInfo(deps) {
476
+ const paths = deps.getBlueprintPaths?.() ?? [];
477
+ if (paths.length === 0) {
478
+ // fallback:如果没有传 getBlueprintPaths,至少返回主蓝图
479
+ if (deps.blueprintUrl) {
480
+ try {
481
+ const localPath = deps.blueprintLocalPath ? resolve(process.cwd(), deps.blueprintLocalPath) : undefined;
482
+ const content = await fetchBlueprintContent(deps.blueprintUrl, localPath);
483
+ const name = basename(deps.blueprintLocalPath ?? deps.blueprintUrl, ".blueprint.md");
484
+ return [{ name, path: deps.blueprintUrl, content }];
485
+ }
486
+ catch {
487
+ return [];
488
+ }
489
+ }
490
+ return [];
491
+ }
492
+ return Promise.all(paths.map(async (p) => {
493
+ const filePath = p.startsWith("file://") ? p.slice(7) : p;
494
+ const name = basename(filePath, ".blueprint.md");
495
+ try {
496
+ const content = readFileSync(filePath, "utf-8");
497
+ return { name, path: p, content };
498
+ }
499
+ catch {
500
+ return { name, path: p, content: null };
501
+ }
502
+ }));
503
+ }
504
+ function getMCPList(mgr) {
505
+ const configs = mgr.getConfigs();
506
+ const servers = [];
507
+ for (const [name, cfg] of configs) {
508
+ const c = cfg;
509
+ servers.push({
510
+ name,
511
+ transport: c.transport ?? "stdio",
512
+ command: c.command,
513
+ args: c.args,
514
+ url: c.url,
515
+ enabled: c.enabled !== false,
516
+ running: mgr.isRunning(name),
517
+ tools: mgr.getToolsForServer(name).map((t) => ({ name: t.name, description: t.description })),
518
+ });
519
+ }
520
+ return { servers };
521
+ }
522
+ function serveUI(res, deps) {
523
+ try {
524
+ let html = readFileSync(resolveStaticFile("app.html"), "utf-8");
525
+ html = html.replace(/__SHOW_CONNECTION_FORM__/g, String(!!deps.configPath));
526
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
527
+ res.end(html);
528
+ }
529
+ catch (e) {
530
+ res.writeHead(500);
531
+ res.end("Failed to load app.html: " + errMsg(e));
532
+ }
533
+ }
534
+ function serveStatic(res, filename) {
535
+ const contentType = filename.endsWith(".css")
536
+ ? "text/css; charset=utf-8"
537
+ : "application/javascript; charset=utf-8";
538
+ try {
539
+ const content = readFileSync(resolveStaticFile(filename), "utf-8");
540
+ res.writeHead(200, { "Content-Type": contentType });
541
+ res.end(content);
542
+ }
543
+ catch {
544
+ res.writeHead(404);
545
+ res.end("Not found: " + filename);
546
+ }
547
+ }
548
+ function getConnectionConfig(configPath) {
549
+ const cfg = readConfig(configPath);
550
+ const url = getNestedValue(cfg, "ailo.wsUrl") ?? "";
551
+ const key = getNestedValue(cfg, "ailo.apiKey") ?? "";
552
+ const id = getNestedValue(cfg, "ailo.endpointId") ?? "";
553
+ const configured = !!(url && key && id);
554
+ return {
555
+ configured,
556
+ ailoWsUrl: url || undefined,
557
+ ailoApiKey: key || undefined,
558
+ endpointId: id || undefined,
559
+ };
560
+ }
561
+ async function saveConnectionConfig(configPath, bodyStr, onSaved) {
562
+ try {
563
+ const bodyTrimmed = (bodyStr ?? "").trim();
564
+ if (!bodyTrimmed)
565
+ return { ok: false, error: "请求体为空" };
566
+ const existing = readConfig(configPath);
567
+ const b = JSON.parse(bodyTrimmed);
568
+ if (b.ailoWsUrl !== undefined)
569
+ setNestedValue(existing, "ailo.wsUrl", b.ailoWsUrl);
570
+ if (b.ailoApiKey !== undefined)
571
+ setNestedValue(existing, "ailo.apiKey", b.ailoApiKey);
572
+ if (b.endpointId !== undefined)
573
+ setNestedValue(existing, "ailo.endpointId", b.endpointId);
574
+ writeConfig(configPath, existing);
575
+ const ailoWsUrl = getNestedValue(existing, "ailo.wsUrl") ?? "";
576
+ const ailoApiKey = getNestedValue(existing, "ailo.apiKey") ?? "";
577
+ const endpointId = getNestedValue(existing, "ailo.endpointId") ?? "";
578
+ if (onSaved && ailoWsUrl && ailoApiKey && endpointId) {
579
+ await onSaved({ ailoWsUrl, ailoApiKey, endpointId });
580
+ return { ok: true, message: "已保存,正在使用新配置重连…" };
581
+ }
582
+ return { ok: true, message: "已保存。" };
583
+ }
584
+ catch (e) {
585
+ return { ok: false, error: errMsg(e) };
586
+ }
587
+ }
588
+ function getEmailConfig(configPath, getStatus) {
589
+ const cfg = readConfig(configPath);
590
+ const email = (cfg.email ?? {});
591
+ const status = getStatus?.() ?? { configured: false, running: false };
592
+ return {
593
+ imapHost: email.imapHost ?? "",
594
+ imapUser: email.imapUser ?? "",
595
+ imapPassword: email.imapPassword ?? "",
596
+ imapPort: email.imapPort ?? 993,
597
+ smtpHost: email.smtpHost ?? "",
598
+ smtpPort: email.smtpPort ?? 465,
599
+ smtpUser: email.smtpUser ?? "",
600
+ smtpPassword: email.smtpPassword ?? "",
601
+ ...status,
602
+ };
603
+ }
604
+ async function saveEmailConfig(configPath, bodyStr, onSaved) {
605
+ try {
606
+ const bodyTrimmed = (bodyStr ?? "").trim();
607
+ if (!bodyTrimmed)
608
+ return { ok: false, error: "请求体为空" };
609
+ const existing = readConfig(configPath);
610
+ const b = JSON.parse(bodyTrimmed);
611
+ const emailFields = ["imapHost", "imapUser", "imapPassword", "imapPort", "smtpHost", "smtpPort", "smtpUser", "smtpPassword"];
612
+ for (const field of emailFields) {
613
+ if (b[field] !== undefined)
614
+ setNestedValue(existing, `email.${field}`, b[field]);
615
+ }
616
+ writeConfig(configPath, existing);
617
+ if (onSaved) {
618
+ await onSaved();
619
+ return { ok: true, message: "已保存,邮件通道正在重启…" };
620
+ }
621
+ return { ok: true, message: "已保存。" };
622
+ }
623
+ catch (e) {
624
+ return { ok: false, error: errMsg(e) };
625
+ }
626
+ }
627
+ function getPlatformConfig(configPath, platform, fields, getStatus) {
628
+ const cfg = readConfig(configPath);
629
+ const section = (cfg[platform] ?? {});
630
+ const status = getStatus?.() ?? { configured: false, running: false };
631
+ const result = { ...status };
632
+ for (const f of fields) {
633
+ result[f] = section[f] ?? "";
634
+ }
635
+ return result;
636
+ }
637
+ async function savePlatformConfig(configPath, platform, fields, bodyStr, onSaved) {
638
+ try {
639
+ const bodyTrimmed = (bodyStr ?? "").trim();
640
+ if (!bodyTrimmed)
641
+ return { ok: false, error: "请求体为空" };
642
+ const existing = readConfig(configPath);
643
+ const b = JSON.parse(bodyTrimmed);
644
+ for (const field of fields) {
645
+ if (b[field] !== undefined)
646
+ setNestedValue(existing, `${platform}.${field}`, b[field]);
647
+ }
648
+ writeConfig(configPath, existing);
649
+ if (onSaved) {
650
+ await onSaved();
651
+ return { ok: true, message: "已保存,通道正在重启…" };
652
+ }
653
+ return { ok: true, message: "已保存。" };
654
+ }
655
+ catch (e) {
656
+ return { ok: false, error: errMsg(e) };
657
+ }
658
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Re-exports from SDK + config.json loading helper for desktop.
3
+ */
4
+ export { hasValidConfig, backoffDelayMs, readConfig, } from "@greatlhd/ailo-endpoint-sdk";
5
+ import { readConfig } from "@greatlhd/ailo-endpoint-sdk";
6
+ export function loadConnectionConfig(configPath) {
7
+ const raw = readConfig(configPath);
8
+ const ailo = raw.ailo;
9
+ return {
10
+ url: process.env.AILO_WS_URL || ailo?.wsUrl || "",
11
+ apiKey: process.env.AILO_API_KEY || ailo?.apiKey || "",
12
+ endpointId: process.env.AILO_ENDPOINT_ID || ailo?.endpointId || "",
13
+ };
14
+ }
@@ -0,0 +1,2 @@
1
+ export const CONFIG_FILENAME = "config.json";
2
+ export const NO_SUBJECT = "(无主题)";