@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/src/index.ts CHANGED
@@ -2,24 +2,35 @@
2
2
  /**
3
3
  * Ailo Desktop — 超级端点
4
4
  *
5
- * 集成桌面能力 + 飞书 + 钉钉 + QQ + 邮件。
6
- * 各平台按需配置:有完整配置才启动对应 handler 并上报对应 blueprint。
5
+ * 集成桌面能力 + 飞书。
6
+ * 各平台按需配置:有完整配置才启动对应 handler 并上报对应工具。
7
7
  * 未配置的平台不会被 Ailo 感知,不影响其他能力正常工作。
8
8
  *
9
- * 子命令:ailo-desktop init [--defaults] — 初始化 config.json 与 Skills 目录(见 cli.ts)。
9
+ * 运行模式:
10
+ * - 桌面模式(有显示器):截图、浏览器、鼠标键盘 + 文件操作、代码执行、MCP
11
+ * - 服务器模式(无显示器):文件操作、代码执行、MCP
12
+ *
13
+ * 子命令:ailo-desktop init [--defaults] [--config-dir <path>] — 初始化 config.json 与 Skills 目录(见 cli.ts)。
10
14
  */
11
15
 
12
16
  const [, , subcommand] = process.argv;
13
17
  if (subcommand === "init") {
14
18
  import("./cli.js")
15
- .then(({ runInit }) =>
16
- runInit(process.argv.includes("--defaults"))
19
+ .then(({ runInit }) => {
20
+ const args = process.argv.slice(2);
21
+ let useDefaults = false;
22
+ let configDir: string | undefined;
23
+ for (let i = 0; i < args.length; i++) {
24
+ if (args[i] === "--defaults") useDefaults = true;
25
+ else if ((args[i] === "--config-dir" || args[i] === "-c") && args[i + 1]) configDir = args[++i];
26
+ }
27
+ return runInit(useDefaults, configDir)
17
28
  .then(() => process.exit(0))
18
29
  .catch((e) => {
19
30
  console.error(e);
20
31
  process.exit(1);
21
- }),
22
- )
32
+ });
33
+ })
23
34
  .catch((e) => {
24
35
  console.error("init 加载失败:", e);
25
36
  process.exit(1);
@@ -27,7 +38,7 @@ if (subcommand === "init") {
27
38
  }
28
39
 
29
40
  import { runEndpoint, type EndpointContext } from "@greatlhd/ailo-endpoint-sdk";
30
- import type { ContentPart, ToolCapability } from "@greatlhd/ailo-endpoint-sdk";
41
+ import type { ContentPart, ToolCapability, SkillMeta } from "@greatlhd/ailo-endpoint-sdk";
31
42
  import { inferMime, classifyMedia, mediaPart } from "@greatlhd/ailo-endpoint-sdk";
32
43
  import { createInterface } from "readline";
33
44
  import { dirname, join } from "path";
@@ -70,29 +81,35 @@ import {
70
81
  import { CONFIG_FILENAME, NO_SUBJECT } from "./constants.js";
71
82
  import { errMsg } from "./utils.js";
72
83
 
73
- const BLUEPRINTS_DIR = join(__dirname, "..", "..", "..", "blueprints");
74
-
75
- function parseArgs(): { port?: number; blueprintUrl?: string } {
84
+ function parseArgs(): { port?: number; configDir?: string; server?: boolean } {
76
85
  const args = process.argv.slice(2);
77
86
  let port: number | undefined;
78
- let blueprintUrl: string | undefined;
87
+ let configDir: string | undefined;
88
+ let server: boolean | undefined;
79
89
  for (let i = 0; i < args.length; i++) {
80
90
  if (args[i] === "--port" && args[i + 1]) {
81
91
  const p = Number(args[++i]);
82
92
  if (!Number.isNaN(p) && p > 0) port = p;
83
- } else if (args[i] === "--blueprint-url" && args[i + 1]) blueprintUrl = args[++i];
93
+ } else if ((args[i] === "--config-dir" || args[i] === "-c") && args[i + 1]) configDir = args[++i];
94
+ else if (args[i] === "--server" || args[i] === "-s") server = true;
84
95
  }
85
- return { port, blueprintUrl };
96
+ return { port, configDir, server };
86
97
  }
87
98
 
88
- const { port: CLI_PORT, blueprintUrl: CLI_BLUEPRINT_URL } = parseArgs();
99
+ const { port: CLI_PORT, configDir: CLI_CONFIG_DIR, server: CLI_SERVER } = parseArgs();
100
+
101
+ function hasDisplay(): boolean {
102
+ if (CLI_SERVER) return false;
103
+ if (process.platform === "darwin" || process.platform === "win32") {
104
+ return true; // macOS/Windows 默认有显示器
105
+ }
106
+ return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
107
+ }
89
108
 
90
- const BLUEPRINT_URL = CLI_BLUEPRINT_URL ?? join(BLUEPRINTS_DIR, "desktop-agent.blueprint.md");
91
- const BLUEPRINT_WEBCHAT = join(BLUEPRINTS_DIR, "webchat.blueprint.md");
92
- const BLUEPRINT_FEISHU = join(BLUEPRINTS_DIR, "feishu.blueprint.md");
109
+ const isDesktopMode = hasDisplay();
93
110
 
94
111
  // ──────────────────────────────────────────────────────────────
95
- // 配置加载:各平台 + email
112
+ // 配置加载:各平台
96
113
  // ──────────────────────────────────────────────────────────────
97
114
 
98
115
  export interface FeishuConfig { appId: string; appSecret: string }
@@ -125,58 +142,70 @@ const mcpManager = new LocalMCPManager();
125
142
  let endpointCtx: EndpointContext | null = null;
126
143
  let webchatApi: { recordAiloReply: (text: string, participantName: string) => boolean } | null = null;
127
144
  let feishuHandler: FeishuHandler | null = null;
128
- let lastMcpToolSnapshot: Map<string, ToolCapability> = new Map();
129
145
  const desktopStateStore = new DesktopStateStore();
130
146
 
131
- function computeToolDiff(oldTools: Map<string, ToolCapability>, newTools: ToolCapability[]): {
132
- register: ToolCapability[];
133
- unregister: string[];
134
- } {
135
- const newMap = new Map(newTools.map((t) => [t.name, t]));
136
- const register: ToolCapability[] = [];
137
- const unregister: string[] = [];
138
- for (const [name] of oldTools) {
139
- if (!newMap.has(name)) unregister.push(name);
140
- }
141
- for (const [name, tool] of newMap) {
142
- if (!oldTools.has(name)) register.push(tool);
143
- }
144
- return { register, unregister };
145
- }
147
+ // 已上报的工具和技能(供 config_server API 使用)
148
+ const reportedEndpointTools: ToolCapability[] = [];
149
+ const reportedEndpointSkills: SkillMeta[] = [];
146
150
 
147
- async function syncMcpToolsToServer(): Promise<void> {
148
- if (!endpointCtx) return;
149
- const currentTools = mcpManager.getAllPrivateTools();
150
- const { register, unregister } = computeToolDiff(lastMcpToolSnapshot, currentTools);
151
- if (register.length === 0 && unregister.length === 0) return;
152
- try {
153
- await endpointCtx.update({
154
- register: register.length > 0 ? { tools: register } : undefined,
155
- unregister: unregister.length > 0 ? { tools: unregister } : undefined,
156
- });
157
- lastMcpToolSnapshot = new Map(currentTools.map((t) => [t.name, t]));
158
- console.log(`[desktop] MCP 工具增量同步: +${register.length} -${unregister.length}`);
159
- } catch (e: unknown) {
160
- console.error("[desktop] MCP 工具增量同步失败:", errMsg(e));
151
+ // ──────────────────────────────────────────────────────────────
152
+ // 端点工具定义(上报给 Ailo)
153
+ // ──────────────────────────────────────────────────────────────
154
+
155
+ function buildEndpointTools(): ToolCapability[] {
156
+ const tools: ToolCapability[] = [];
157
+
158
+ // 文件操作工具
159
+ const fsToolDefs: [string, string][] = [
160
+ ["read_file", "读取文件内容。传入文件路径,返回文件内容。"],
161
+ ["write_file", "创建或覆写文件。将内容写入指定路径的文件。"],
162
+ ["edit_file", "编辑文件。在指定位置插入内容。"],
163
+ ["append_file", "追加内容。将内容追加到文件末尾。"],
164
+ ["list_directory", "列出目录内容。返回指定目录下的文件和子目录列表。"],
165
+ ["find_files", "查找文件。根据模式匹配文件名。"],
166
+ ["search_content", "搜索内容。在文件中搜索匹配的文本行。"],
167
+ ["delete_file", "删除文件。删除指定路径的文件。"],
168
+ ["move_file", "移动文件。将文件从源路径移动到目标路径。"],
169
+ ["copy_file", "复制文件。将文件从源路径复制到目标路径。"],
170
+ ];
171
+ for (const [name, desc] of fsToolDefs) {
172
+ tools.push({ name, description: desc });
161
173
  }
162
- }
163
174
 
164
- async function initSubsystems(): Promise<void> {
165
- try {
166
- await mcpManager.init();
167
- mcpManager.startWatching();
168
- let mcpSyncTimer: ReturnType<typeof setTimeout> | null = null;
169
- mcpManager.setOnToolsChanged(() => {
170
- if (mcpSyncTimer) return; // 防抖:500ms 内只同步一次
171
- mcpSyncTimer = setTimeout(() => {
172
- mcpSyncTimer = null;
173
- console.log("[desktop] MCP 工具变更,增量同步到 Ailo");
174
- syncMcpToolsToServer();
175
- }, 500);
176
- });
177
- } catch (e: unknown) {
178
- console.error("[desktop] MCP 初始化失败:", errMsg(e));
175
+ // 代码执行工具
176
+ tools.push(
177
+ { name: "execute_code", description: "执行代码。支持多种编程语言的代码执行。" },
178
+ { name: "exec", description: "执行 shell 命令。在服务器上执行 shell 命令并返回输出。" },
179
+ );
180
+
181
+ // MCP 管理工具
182
+ tools.push({
183
+ name: "mcp_manage",
184
+ description: "管理 MCP 服务器。列出、启动、停止 MCP 服务器,以及查看服务器提供的工具。",
185
+ });
186
+
187
+ // 飞书发送工具(飞书配置后可用)
188
+ tools.push({
189
+ name: "feishu_send",
190
+ description: "通过飞书发送消息。支持发送给指定 chat_id,可附带文本和文件附件。",
191
+ });
192
+
193
+ // 网页聊天发送工具
194
+ tools.push({
195
+ name: "webchat_send",
196
+ description: "通过网页聊天界面发送消息给用户。需要在浏览器中打开 Ailo Desktop 配置页。",
197
+ });
198
+
199
+ // 桌面模式工具
200
+ if (isDesktopMode) {
201
+ tools.push(
202
+ { name: "screenshot", description: "截取屏幕截图。可指定窗口或全屏。" },
203
+ { name: "browser_use", description: "浏览器自动化。控制浏览器进行网页操作。" },
204
+ { name: "mouse_keyboard", description: "鼠标键盘控制。模拟鼠标点击、键盘输入等操作。" },
205
+ );
179
206
  }
207
+
208
+ return tools;
180
209
  }
181
210
 
182
211
  // ──────────────────────────────────────────────────────────────
@@ -201,13 +230,38 @@ async function stopPlatformHandlers(): Promise<void> {
201
230
  }
202
231
 
203
232
  // ──────────────────────────────────────────────────────────────
204
- // 动态计算 blueprints(只注册已配置的平台)
233
+ // 同步 MCP 工具到服务端
205
234
  // ──────────────────────────────────────────────────────────────
206
235
 
207
- function buildBlueprints(configPath: string): string[] {
208
- const list: string[] = [BLUEPRINT_URL, BLUEPRINT_WEBCHAT];
209
- if (loadFeishuConfig(configPath)) list.push(BLUEPRINT_FEISHU);
210
- return list;
236
+ async function syncMcpToolsToServer(): Promise<void> {
237
+ if (!endpointCtx) return;
238
+ const mcpTools = mcpManager.getAllPrivateTools();
239
+ try {
240
+ await endpointCtx.update({
241
+ register: { mcpTools },
242
+ });
243
+ console.log(`[desktop] MCP 工具同步: ${mcpTools.length} 个工具`);
244
+ } catch (e: unknown) {
245
+ console.error("[desktop] MCP 工具同步失败:", errMsg(e));
246
+ }
247
+ }
248
+
249
+ async function initSubsystems(): Promise<void> {
250
+ try {
251
+ await mcpManager.init();
252
+ mcpManager.startWatching();
253
+ let mcpSyncTimer: ReturnType<typeof setTimeout> | null = null;
254
+ mcpManager.setOnToolsChanged(() => {
255
+ if (mcpSyncTimer) return;
256
+ mcpSyncTimer = setTimeout(() => {
257
+ mcpSyncTimer = null;
258
+ console.log("[desktop] MCP 工具变更,同步到 Ailo");
259
+ syncMcpToolsToServer();
260
+ }, 500);
261
+ });
262
+ } catch (e: unknown) {
263
+ console.error("[desktop] MCP 初始化失败:", errMsg(e));
264
+ }
211
265
  }
212
266
 
213
267
  // ──────────────────────────────────────────────────────────────
@@ -227,71 +281,78 @@ function requireHandler<T>(handler: T | null, platform: string): T {
227
281
  const requireFeishu = () => requireHandler(feishuHandler, "飞书");
228
282
 
229
283
  function buildToolHandlers(): Record<string, (args: Record<string, unknown>) => Promise<ContentPart[] | unknown>> {
230
- const handlers: Record<string, (args: Record<string, unknown>) => Promise<ContentPart[] | unknown>> = {
231
- screenshot: async (args) => {
284
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<ContentPart[] | unknown>> = {};
285
+
286
+ // 桌面模式工具(需要显示器)
287
+ if (isDesktopMode) {
288
+ handlers.screenshot = async (args) => {
232
289
  const result = await captureDesktopObservation({
233
290
  capture_window: !!args.capture_window,
234
291
  screen: args.screen as number | "all" | undefined,
235
292
  });
236
293
  if (result.observation) desktopStateStore.saveObservation(result.observation);
237
294
  return result.parts;
238
- },
239
- browser_use: async (args) => browserUse(args),
240
- execute_code: async (args) => {
241
- if (!endpointCtx) throw new Error("端点未就绪");
242
- return executeCode(endpointCtx, args);
243
- },
244
- exec: async (args) => {
245
- if (!endpointCtx) throw new Error("端点未就绪");
246
- return execTool(endpointCtx, args);
247
- },
248
- mouse_keyboard: async (args) => mouseKeyboard(args, { stateStore: desktopStateStore }),
249
- mcp_manage: async (args) => {
250
- const result = await mcpManager.handle(args);
251
- if (result.toolsChanged) syncMcpToolsToServer();
252
- return result.text;
253
- },
254
- // 网页聊天发送
255
- webchat_send: async (args) => {
256
- const text = args.text as string | undefined;
257
- const participantName = args.participantName as string;
258
- const attachments = (args.attachments as { path?: string }[]) ?? [];
259
- if (!webchatApi && attachments.length === 0) {
260
- return [{ type: "text", text: JSON.stringify({ ok: false, error: "webchat未就绪,请先连接Ailo并打开配置页" }) }];
261
- }
262
- const results: string[] = [];
263
- // 发送文字
264
- if (text && webchatApi) {
265
- const ok = webchatApi.recordAiloReply(text, participantName ?? "");
266
- results.push(ok ? "文字已发送" : "文字发送失败,请确认participantName与网页聊天中的称呼一致且用户在线");
267
- }
268
- // 发送附件
269
- if (attachments.length > 0) {
270
- if (!endpointCtx) return [{ type: "text", text: JSON.stringify({ ok: false, error: "端点未就绪,无法发送附件" }) }];
271
- for (const att of attachments) {
272
- if (att.path) {
273
- await endpointCtx.sendFile(att.path);
274
- results.push(`文件已发送:${att.path}`);
275
- }
295
+ };
296
+ handlers.browser_use = async (args) => browserUse(args);
297
+ handlers.mouse_keyboard = async (args) => mouseKeyboard(args, { stateStore: desktopStateStore });
298
+ }
299
+
300
+ // 通用工具(桌面和服务器都可用)
301
+ handlers.execute_code = async (args) => {
302
+ if (!endpointCtx) throw new Error("端点未就绪");
303
+ return executeCode(endpointCtx, args);
304
+ };
305
+ handlers.exec = async (args) => {
306
+ if (!endpointCtx) throw new Error("端点未就绪");
307
+ return execTool(endpointCtx, args);
308
+ };
309
+ handlers.mcp_manage = async (args) => {
310
+ const result = await mcpManager.handle(args);
311
+ if (result.toolsChanged) syncMcpToolsToServer();
312
+ return result.text;
313
+ };
314
+
315
+ // 网页聊天发送
316
+ handlers.webchat_send = async (args) => {
317
+ const text = args.text as string | undefined;
318
+ const participantName = args.participantName as string;
319
+ const attachments = (args.attachments as { path?: string }[]) ?? [];
320
+ if (!webchatApi && attachments.length === 0) {
321
+ return [{ type: "text", text: JSON.stringify({ ok: false, error: "webchat未就绪,请先连接Ailo并打开配置页" }) }];
322
+ }
323
+ const results: string[] = [];
324
+ if (text && webchatApi) {
325
+ const ok = webchatApi.recordAiloReply(text, participantName ?? "");
326
+ results.push(ok ? "文字已发送" : "文字发送失败,请确认participantName与网页聊天中的称呼一致且用户在线");
327
+ }
328
+ if (attachments.length > 0) {
329
+ if (!endpointCtx) return [{ type: "text", text: JSON.stringify({ ok: false, error: "端点未就绪,无法发送附件" }) }];
330
+ for (const att of attachments) {
331
+ if (att.path) {
332
+ await endpointCtx.sendFile(att.path);
333
+ results.push(`文件已发送:${att.path}`);
276
334
  }
277
335
  }
278
- if (results.length === 0) return [{ type: "text", text: JSON.stringify({ ok: false, error: "请提供text或attachments" }) }];
279
- return [{ type: "text", text: JSON.stringify({ ok: true, results }) }];
280
- },
281
- // 飞书发送
282
- feishu_send: async (args) => {
283
- const h = requireFeishu();
284
- const atts = ((args.attachments as any[]) ?? []).map((a: any) => ({
285
- type: a.type,
286
- file_path: a.path,
287
- mime: a.mime,
288
- name: a.name,
289
- duration: a.duration,
290
- }));
291
- await h.sendText(args.chat_id as string, (args.text as string) ?? "", atts);
292
- return [{ type: "text", text: `已发送到 ${args.chat_id}` }];
293
- },
336
+ }
337
+ if (results.length === 0) return [{ type: "text", text: JSON.stringify({ ok: false, error: "请提供text或attachments" }) }];
338
+ return [{ type: "text", text: JSON.stringify({ ok: true, results }) }];
339
+ };
340
+
341
+ // 飞书发送
342
+ handlers.feishu_send = async (args) => {
343
+ const h = requireFeishu();
344
+ const atts = ((args.attachments as any[]) ?? []).map((a: any) => ({
345
+ type: a.type,
346
+ file_path: a.path,
347
+ mime: a.mime,
348
+ name: a.name,
349
+ duration: a.duration,
350
+ }));
351
+ await h.sendText(args.chat_id as string, (args.text as string) ?? "", atts);
352
+ return [{ type: "text", text: `已发送到 ${args.chat_id}` }];
294
353
  };
354
+
355
+ // 文件系统工具
295
356
  for (const name of FS_TOOLS) {
296
357
  handlers[name] = async (args) => fsTool(name, args);
297
358
  }
@@ -304,38 +365,19 @@ function buildToolHandlers(): Record<string, (args: Record<string, unknown>) =>
304
365
 
305
366
  async function main(): Promise<void> {
306
367
  const port = CLI_PORT ?? (await promptPort());
307
- const configPath = join(process.cwd(), CONFIG_FILENAME);
368
+ const configDir = CLI_CONFIG_DIR ?? process.cwd();
369
+ const configPath = join(configDir, CONFIG_FILENAME);
370
+ console.log(`[desktop] 配置目录: ${configDir}`);
371
+ console.log(`[desktop] 运行模式: ${isDesktopMode ? "桌面模式" : "服务器模式"}`);
308
372
  const connectionState = { connected: false, endpointId: "" };
309
373
  let webchatCtxRef: EndpointContext | null = null;
310
374
  let connectAttempt = 0;
311
375
  let endpointConnecting = false;
312
376
 
313
- async function reloadPlatformHandler<
314
- C,
315
- H extends { start(ctx: EndpointContext): Promise<void>; stop(): Promise<void> },
316
- >(opts: {
317
- getHandler: () => H | null;
318
- setHandler: (h: H | null) => void;
319
- loadConfig: () => C | null;
320
- createHandler: (cfg: C) => H;
321
- blueprint: string;
322
- }): Promise<void> {
323
- if (!endpointCtx) return;
324
- const wasRunning = !!opts.getHandler();
325
- if (opts.getHandler()) {
326
- await opts.getHandler()!.stop();
327
- opts.setHandler(null);
328
- }
329
- const cfg = opts.loadConfig();
330
- if (cfg) {
331
- const h = opts.createHandler(cfg);
332
- opts.setHandler(h);
333
- await h.start(endpointCtx);
334
- if (!wasRunning) await endpointCtx.update({ register: { blueprints: [opts.blueprint] } });
335
- } else if (wasRunning) {
336
- await endpointCtx.update({ unregister: { blueprints: [opts.blueprint] } });
337
- }
338
- }
377
+ // 端点描述
378
+ const endpointInstructions = isDesktopMode
379
+ ? "Ailo Desktop 端点 - 具备文件操作、代码执行、截图、浏览器自动化、鼠标键盘控制等能力"
380
+ : "Ailo Server 端点 - 具备文件操作、代码执行等服务器能力";
339
381
 
340
382
  async function applyConnectionConfig(overrides?: AiloConnectionConfig): Promise<void> {
341
383
  const cfg = overrides ?? loadConnectionConfig(configPath);
@@ -344,8 +386,13 @@ async function main(): Promise<void> {
344
386
 
345
387
  endpointConnecting = true;
346
388
  connectAttempt = 0;
389
+
390
+ // 填充已上报的工具列表
391
+ const builtInTools = buildEndpointTools();
392
+ reportedEndpointTools.length = 0;
393
+ reportedEndpointTools.push(...builtInTools);
394
+
347
395
  try {
348
- const blueprints = buildBlueprints(configPath);
349
396
  runEndpoint({
350
397
  ailoWsUrl: cfg.url,
351
398
  ailoApiKey: cfg.apiKey,
@@ -358,10 +405,11 @@ async function main(): Promise<void> {
358
405
  connectionState.connected = true;
359
406
  connectionState.endpointId = cfg.endpointId;
360
407
  webchatCtxRef = ctx;
361
- lastMcpToolSnapshot = new Map(mcpManager.getAllPrivateTools().map((t) => [t.name, t]));
362
408
  serverRef.notifyContextAttached();
363
409
  await startPlatformHandlers(ctx, configPath);
364
- console.log("[desktop] 桌面端点已启动");
410
+ // 同步 MCP 工具
411
+ await syncMcpToolsToServer();
412
+ console.log("[desktop] 端点已启动");
365
413
  },
366
414
  stop: async () => {
367
415
  await stopPlatformHandlers();
@@ -370,14 +418,17 @@ async function main(): Promise<void> {
370
418
  connectionState.connected = false;
371
419
  connectionState.endpointId = "";
372
420
  webchatCtxRef = null;
421
+ reportedEndpointTools.length = 0;
422
+ reportedEndpointSkills.length = 0;
373
423
  await mcpManager.shutdown();
374
- await stopBrowser();
375
- console.log("[desktop] 桌面端点已停止");
424
+ if (isDesktopMode) await stopBrowser();
425
+ console.log("[desktop] 端点已停止");
376
426
  },
377
427
  },
378
428
  caps: ["message", "tool_execute"],
379
- blueprints,
380
- tools: mcpManager.getAllPrivateTools(),
429
+ tools: builtInTools,
430
+ mcpTools: mcpManager.getAllPrivateTools(),
431
+ instructions: endpointInstructions,
381
432
  toolHandlers: buildToolHandlers(),
382
433
  onUnknownTool: async (name: string, args: Record<string, unknown>) => {
383
434
  const idx = name.indexOf(":");
@@ -400,11 +451,7 @@ async function main(): Promise<void> {
400
451
  return;
401
452
  }
402
453
  try {
403
- await client.reconnect(undefined, {
404
- url: latest.url,
405
- apiKey: latest.apiKey,
406
- endpointId: latest.endpointId,
407
- });
454
+ await client.reconnect(undefined, undefined, undefined, mcpManager.getAllPrivateTools());
408
455
  } catch (e) {
409
456
  console.error("[desktop] 重试连接失败:", errMsg(e));
410
457
  }
@@ -422,15 +469,15 @@ async function main(): Promise<void> {
422
469
  mcpManager,
423
470
  getConnectionStatus: () => connectionState,
424
471
  getWebchatCtx: () => webchatCtxRef,
472
+ getEndpointCtx: () => endpointCtx,
473
+ getEndpointTools: () => reportedEndpointTools.map(t => ({ name: t.name, description: t.description ?? "" })),
474
+ getEndpointSkills: () => reportedEndpointSkills.map(s => ({ name: s.name, description: s.description ?? "" })),
425
475
  port,
426
476
  configPath,
427
- blueprintUrl: BLUEPRINT_URL,
428
- blueprintLocalPath: join(BLUEPRINTS_DIR, "desktop-agent.blueprint.md"),
429
- getBlueprintPaths: () => buildBlueprints(configPath),
430
477
  onWebchatReady: (api) => { webchatApi = api; },
431
478
  onRequestReconnect: async () => {
432
479
  if (!endpointCtx) return;
433
- await endpointCtx.client.reconnect([]);
480
+ await endpointCtx.client.reconnect(undefined, undefined, undefined, mcpManager.getAllPrivateTools());
434
481
  },
435
482
  onConnectionConfigSaved: async (config) => {
436
483
  const cfg: AiloConnectionConfig = {
@@ -439,18 +486,15 @@ async function main(): Promise<void> {
439
486
  endpointId: config.endpointId,
440
487
  };
441
488
  if (endpointCtx) {
442
- await endpointCtx.client.reconnect(undefined, cfg);
489
+ await endpointCtx.client.reconnect(undefined, cfg, undefined, mcpManager.getAllPrivateTools());
443
490
  } else {
444
491
  await applyConnectionConfig(cfg);
445
492
  }
446
493
  },
447
- onFeishuConfigSaved: () => reloadPlatformHandler({
448
- getHandler: () => feishuHandler,
449
- setHandler: (h) => { feishuHandler = h; },
450
- loadConfig: () => loadFeishuConfig(configPath),
451
- createHandler: (cfg) => new FeishuHandler(cfg),
452
- blueprint: BLUEPRINT_FEISHU,
453
- }),
494
+ onFeishuConfigSaved: async () => {
495
+ // 飞书配置变更时,重新同步 MCP 工具(可能包含新的飞书工具)
496
+ await syncMcpToolsToServer();
497
+ },
454
498
  getFeishuStatus: () => ({
455
499
  configured: !!loadFeishuConfig(configPath),
456
500
  running: !!feishuHandler,