@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@greatlhd/ailo-desktop",
3
- "version": "1.0.0",
4
- "description": "Ailo 超级端点 — 桌面能力 + 飞书 + 钉钉 + QQ + 邮件,按需配置启用",
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.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
- const CONFIG_PATH = join(process.cwd(), CONFIG_FILENAME);
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(useDefaults = false): Promise<void> {
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(CONFIG_PATH, config);
40
- console.log(`\n已写入 ${CONFIG_PATH}`);
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
- console.log("\n初始化完成!运行 ailo-desktop 启动桌面端点。");
43
- console.log("配置界面端口:启动时用 --port <端口> 指定,或运行后在控制台按提示输入。");
69
+ if (import.meta.url === `file://${process.argv[1]}`) {
70
+ runInit().catch(console.error);
44
71
  }
@@ -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, basename } from "path";
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/blueprint" && req.method === "GET") return json(res, await getBlueprintInfo(deps));
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
- if (deps.blueprintUrl) {
488
- try {
489
- const localPath = deps.blueprintLocalPath
490
- ? resolve(process.cwd(), deps.blueprintLocalPath)
491
- : undefined;
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 getBlueprintInfo(deps: ConfigServerDeps): Promise<{ url: string | null; content: string | null }> {
507
- if (!deps.blueprintUrl) return { url: null, content: null };
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
- async function getAllBlueprintsInfo(deps: ConfigServerDeps): Promise<{ name: string; path: string; content: string | null }[]> {
518
- const paths = deps.getBlueprintPaths?.() ?? [];
519
- if (paths.length === 0) {
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
- return Promise.all(
534
- paths.map(async (p) => {
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,