@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,182 @@
1
+ import { spawn, type ChildProcess } from "child_process";
2
+ import type { ToolCapability } from "@greatlhd/ailo-endpoint-sdk";
3
+
4
+ export interface PendingRPC {
5
+ resolve: (v: unknown) => void;
6
+ reject: (e: Error) => void;
7
+ }
8
+
9
+ export interface StdioSession {
10
+ kind: "stdio";
11
+ proc: ChildProcess;
12
+ tools: ToolCapability[];
13
+ buffer: string;
14
+ nextId: number;
15
+ pendingRequests: Map<number, PendingRPC>;
16
+ config?: MCPServerConfig;
17
+ }
18
+
19
+ export interface SSESession {
20
+ kind: "sse";
21
+ tools: ToolCapability[];
22
+ nextId: number;
23
+ pendingRequests: Map<number, PendingRPC>;
24
+ messageEndpoint: string;
25
+ abortController: AbortController;
26
+ config?: MCPServerConfig;
27
+ }
28
+
29
+ export type MCPSession = StdioSession | SSESession;
30
+
31
+ export interface MCPServerConfig {
32
+ transport?: "stdio" | "sse";
33
+ command?: string;
34
+ args?: string[];
35
+ env?: Record<string, string>;
36
+ url?: string;
37
+ enabled?: boolean;
38
+ }
39
+
40
+ function shellEscape(arg: string): string {
41
+ if (process.platform === "win32") {
42
+ if (!/[ "&|<>^%!]/.test(arg)) return arg;
43
+ return `"${arg.replace(/"/g, '""')}"`;
44
+ }
45
+ if (!/[^a-zA-Z0-9_./:=@-]/.test(arg)) return arg;
46
+ return `'${arg.replace(/'/g, "'\\''")}'`;
47
+ }
48
+
49
+ export async function startStdioServer(
50
+ config: MCPServerConfig,
51
+ onExit: (code: number | null) => void,
52
+ ): Promise<StdioSession> {
53
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", ...(config.env ?? {}) };
54
+ const args = config.args ?? [];
55
+ const shellCmd = [config.command!, ...args].map(shellEscape).join(" ");
56
+ const proc = spawn(shellCmd, [], {
57
+ stdio: ["pipe", "pipe", "pipe"],
58
+ env,
59
+ shell: true,
60
+ });
61
+
62
+ const session: StdioSession = {
63
+ kind: "stdio",
64
+ proc,
65
+ tools: [],
66
+ buffer: "",
67
+ nextId: 1,
68
+ pendingRequests: new Map(),
69
+ };
70
+
71
+ proc.stderr?.on("data", (chunk: Buffer) => {
72
+ const text = chunk.toString("utf-8").trim();
73
+ if (text) console.error(`[mcp:stderr]`, text);
74
+ });
75
+
76
+ proc.on("exit", (code) => {
77
+ onExit(code);
78
+ for (const [, pending] of session.pendingRequests) {
79
+ pending.reject(new Error(`MCP process exited`));
80
+ }
81
+ session.pendingRequests.clear();
82
+ });
83
+
84
+ return session;
85
+ }
86
+
87
+ export async function startSSEServer(
88
+ config: MCPServerConfig,
89
+ onMessage: (session: SSESession, data: string) => void,
90
+ onEnd: () => void,
91
+ ): Promise<SSESession> {
92
+ const baseUrl = config.url!.replace(/\/+$/, "");
93
+ const sseUrl = `${baseUrl}/sse`;
94
+ const abortController = new AbortController();
95
+
96
+ const session: SSESession = {
97
+ kind: "sse",
98
+ tools: [],
99
+ nextId: 1,
100
+ pendingRequests: new Map(),
101
+ messageEndpoint: "",
102
+ abortController,
103
+ };
104
+
105
+ const response = await fetch(sseUrl, {
106
+ signal: abortController.signal,
107
+ headers: { Accept: "text/event-stream" },
108
+ });
109
+ if (!response.ok) throw new Error(`SSE connect failed: HTTP ${response.status}`);
110
+ if (!response.body) throw new Error("SSE response has no body");
111
+
112
+ const reader = response.body.getReader();
113
+ const decoder = new TextDecoder();
114
+ let sseBuffer = "";
115
+ let endpointResolved = false;
116
+
117
+ const readLoop = async () => {
118
+ while (true) {
119
+ const { done, value } = await reader.read();
120
+ if (done) {
121
+ if (!abortController.signal.aborted) {
122
+ onEnd();
123
+ for (const [, pending] of session.pendingRequests) {
124
+ pending.reject(new Error(`SSE stream ended`));
125
+ }
126
+ }
127
+ break;
128
+ }
129
+ sseBuffer += decoder.decode(value, { stream: true });
130
+ const lines = sseBuffer.split("\n");
131
+ sseBuffer = lines.pop() ?? "";
132
+ let currentEvent = "";
133
+ let currentData = "";
134
+ for (const line of lines) {
135
+ if (line.startsWith("event:")) {
136
+ currentEvent = line.slice(6).trim();
137
+ } else if (line.startsWith("data:")) {
138
+ currentData = line.slice(5).trim();
139
+ } else if (line.trim() === "" && currentEvent) {
140
+ if (currentEvent === "endpoint" && !endpointResolved) {
141
+ endpointResolved = true;
142
+ let ep = currentData;
143
+ if (ep.startsWith("/")) ep = `${baseUrl}${ep}`;
144
+ session.messageEndpoint = ep;
145
+ } else if (currentEvent === "message") {
146
+ onMessage(session, currentData);
147
+ }
148
+ currentEvent = "";
149
+ currentData = "";
150
+ }
151
+ }
152
+ }
153
+ };
154
+
155
+ readLoop().catch((err) => {
156
+ if (!abortController.signal.aborted) {
157
+ console.error(`[mcp:sse] read error:`, err.message);
158
+ onEnd();
159
+ }
160
+ });
161
+
162
+ return session;
163
+ }
164
+
165
+ export function stopSession(session: MCPSession): Promise<void> {
166
+ if (session.kind === "stdio") {
167
+ try { session.proc.kill("SIGTERM"); } catch {}
168
+ return new Promise((resolve) => {
169
+ const timer = setTimeout(() => {
170
+ try { session.proc.kill("SIGKILL"); } catch {}
171
+ resolve();
172
+ }, 3000);
173
+ session.proc.once("exit", () => { clearTimeout(timer); resolve(); });
174
+ });
175
+ } else {
176
+ session.abortController.abort();
177
+ for (const [, pending] of session.pendingRequests) {
178
+ pending.reject(new Error(`SSE session stopped`));
179
+ }
180
+ return Promise.resolve();
181
+ }
182
+ }
@@ -0,0 +1,273 @@
1
+ import { MCPConfigManager, startStdioServer, startSSEServer, stopSession, initializeSession, createStdioRpc, createSSERpc, handleSSEMessage } from "./mcp/index.js";
2
+ import type { MCPServerConfig, MCPSession, StdioSession, SSESession } from "./mcp/index.js";
3
+ import type { ToolCapability } from "@greatlhd/ailo-endpoint-sdk";
4
+
5
+ type Args = Record<string, unknown>;
6
+
7
+ export class LocalMCPManager {
8
+ private configManager = new MCPConfigManager();
9
+ private sessions = new Map<string, MCPSession>();
10
+ private watchTimer: ReturnType<typeof setInterval> | null = null;
11
+ private onToolsChanged: (() => void) | null = null;
12
+
13
+ setOnToolsChanged(cb: () => void): void {
14
+ this.onToolsChanged = cb;
15
+ }
16
+
17
+ async init(): Promise<void> {
18
+ await this.configManager.load();
19
+ for (const [name, cfg] of this.configManager.getAll()) {
20
+ if (cfg.enabled !== false) {
21
+ await this.startServer(name, cfg).catch((e) => console.error(`[mcp] failed to start ${name}:`, e.message));
22
+ }
23
+ }
24
+ }
25
+
26
+ startWatching(intervalMs = 2000): void {
27
+ if (this.watchTimer) return;
28
+ this.watchTimer = setInterval(() => this.checkConfigChange(), intervalMs);
29
+ }
30
+
31
+ stopWatching(): void {
32
+ if (this.watchTimer) { clearInterval(this.watchTimer); this.watchTimer = null; }
33
+ }
34
+
35
+ async handle(args: Args): Promise<{ text: string; toolsChanged: boolean }> {
36
+ const action = args.action as string;
37
+ switch (action) {
38
+ case "list": return { text: this.doList(), toolsChanged: false };
39
+ case "create": return await this.doCreate(args);
40
+ case "delete": return await this.doDelete(args);
41
+ case "start": return await this.doStart(args);
42
+ case "stop": return await this.doStop(args);
43
+ default: throw new Error(`未知 action: ${action}`);
44
+ }
45
+ }
46
+
47
+ getAllPrivateTools(): ToolCapability[] {
48
+ const all: ToolCapability[] = [];
49
+ for (const session of this.sessions.values()) {
50
+ all.push(...session.tools);
51
+ }
52
+ return all;
53
+ }
54
+
55
+ async shutdown(): Promise<void> {
56
+ this.stopWatching();
57
+ for (const [name] of this.sessions) {
58
+ await this.stopServer(name);
59
+ }
60
+ }
61
+
62
+ getConfigs(): Map<string, MCPServerConfig> {
63
+ return this.configManager.getAll();
64
+ }
65
+
66
+ isRunning(name: string): boolean {
67
+ return this.sessions.has(name);
68
+ }
69
+
70
+ getToolsForServer(name: string): ToolCapability[] {
71
+ return this.sessions.get(name)?.tools ?? [];
72
+ }
73
+
74
+ private async checkConfigChange(): Promise<void> {
75
+ const changed = await this.configManager.checkChanged();
76
+ if (!changed) return;
77
+
78
+ console.log("[mcp] config file changed, reloading...");
79
+ await this.configManager.load();
80
+ const newConfigs = this.configManager.getAll();
81
+ let toolsChanged = false;
82
+
83
+ for (const [name] of this.sessions) {
84
+ if (!newConfigs.has(name) || newConfigs.get(name)?.enabled === false) {
85
+ await this.stopServer(name);
86
+ toolsChanged = true;
87
+ }
88
+ }
89
+
90
+ for (const [name, cfg] of newConfigs) {
91
+ if (cfg.enabled === false) continue;
92
+ const existing = this.sessions.get(name);
93
+ if (!existing) {
94
+ await this.startServer(name, cfg).catch((e) =>
95
+ console.error(`[mcp] reload failed for ${name}:`, e.message),
96
+ );
97
+ toolsChanged = true;
98
+ }
99
+ }
100
+
101
+ if (toolsChanged) this.onToolsChanged?.();
102
+ }
103
+
104
+ private async startServer(name: string, config: MCPServerConfig): Promise<void> {
105
+ if (this.sessions.has(name)) await this.stopServer(name);
106
+
107
+ const transport = config.transport ?? "stdio";
108
+ if (transport === "sse") {
109
+ await this.startSSEServer(name, config);
110
+ } else {
111
+ await this.startStdioServer(name, config);
112
+ }
113
+ }
114
+
115
+ private async startStdioServer(name: string, config: MCPServerConfig): Promise<void> {
116
+ const session = await startStdioServer(config, (code) => {
117
+ console.log(`[mcp] ${name} exited with code ${code}`);
118
+ this.sessions.delete(name);
119
+ });
120
+
121
+ session.config = config;
122
+ this.sessions.set(name, session);
123
+
124
+ const { request, notify, processBuffer } = createStdioRpc(session);
125
+ session.proc.stdout!.on("data", (chunk: Buffer) => {
126
+ session.buffer += chunk.toString("utf-8");
127
+ processBuffer();
128
+ });
129
+
130
+ await this.initMCPSession(name, session, request, notify);
131
+ }
132
+
133
+ private async startSSEServer(name: string, config: MCPServerConfig): Promise<void> {
134
+ const session = await startSSEServer(config, (sess, data) => {
135
+ handleSSEMessage(sess, data);
136
+ }, () => {
137
+ console.log(`[mcp:sse] ${name} SSE stream ended`);
138
+ this.sessions.delete(name);
139
+ });
140
+
141
+ session.config = config;
142
+ this.sessions.set(name, session);
143
+ console.log(`[mcp:sse] ${name} connected, message endpoint: ${session.messageEndpoint}`);
144
+
145
+ const { request, notify } = createSSERpc(session);
146
+ await this.initMCPSession(name, session, request, notify);
147
+ }
148
+
149
+ private async initMCPSession(
150
+ name: string,
151
+ session: MCPSession,
152
+ request: (method: string, params: unknown) => Promise<unknown>,
153
+ notify: (method: string, params: unknown) => void,
154
+ ): Promise<void> {
155
+ try {
156
+ const tools = await initializeSession(session, request, notify);
157
+ session.tools = tools.map((t) => ({
158
+ name: `${name}:${t.name}`,
159
+ description: t.description,
160
+ parameters: t.parameters,
161
+ }));
162
+ console.log(`[mcp] ${name} started (${session.kind}), discovered ${session.tools.length} tool(s)`);
163
+ } catch (e: unknown) {
164
+ const msg = e instanceof Error ? e.message : String(e);
165
+ console.error(`[mcp] ${name} init failed:`, msg);
166
+ await this.stopServer(name);
167
+ throw e;
168
+ }
169
+ }
170
+
171
+ private async stopServer(name: string): Promise<void> {
172
+ const session = this.sessions.get(name);
173
+ if (!session) return;
174
+ this.sessions.delete(name);
175
+ await stopSession(session);
176
+ }
177
+
178
+ async executeToolCall(serverName: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
179
+ const session = this.sessions.get(serverName);
180
+ if (!session) throw new Error(`MCP server not running: ${serverName}`);
181
+
182
+ if (session.kind === "stdio") {
183
+ const { request } = createStdioRpc(session);
184
+ return request("tools/call", { name: toolName, arguments: args });
185
+ } else {
186
+ const { request } = createSSERpc(session);
187
+ return request("tools/call", { name: toolName, arguments: args });
188
+ }
189
+ }
190
+
191
+ private doList(): string {
192
+ const configs = this.configManager.getAll();
193
+ if (configs.size === 0) return "没有已注册的 MCP 服务";
194
+ const lines: string[] = ["MCP 服务列表:"];
195
+ for (const [name, cfg] of configs) {
196
+ const session = this.sessions.get(name);
197
+ const status = session ? "运行中" : (cfg.enabled !== false ? "已停止" : "已禁用");
198
+ const transport = cfg.transport ?? "stdio";
199
+ const toolCount = session?.tools.length ?? 0;
200
+ lines.push(`- ${name}: ${status} | ${transport} | ${toolCount} 工具`);
201
+ if (session && session.tools.length > 0) {
202
+ for (const t of session.tools) {
203
+ lines.push(` - ${t.name}: ${t.description ?? ""}`);
204
+ }
205
+ }
206
+ }
207
+ return lines.join("\n");
208
+ }
209
+
210
+ private async doCreate(args: Args): Promise<{ text: string; toolsChanged: boolean }> {
211
+ const name = args.name as string;
212
+ if (!name) throw new Error("name 必填");
213
+ const transport = (args.transport as string) ?? "stdio";
214
+ let config: MCPServerConfig;
215
+ if (transport === "sse") {
216
+ const url = args.url as string;
217
+ if (!url) throw new Error("SSE 模式下 url 必填");
218
+ config = { transport: "sse", url, env: args.env as Record<string, string>, enabled: true };
219
+ } else {
220
+ const command = args.command as string;
221
+ if (!command) throw new Error("command 必填");
222
+ config = { transport: "stdio", command, args: args.args as string[], env: args.env as Record<string, string>, enabled: true };
223
+ }
224
+ this.configManager.set(name, config);
225
+ await this.configManager.save();
226
+ try {
227
+ await this.startServer(name, config);
228
+ const session = this.sessions.get(name);
229
+ const toolCount = session?.tools.length ?? 0;
230
+ return { text: `已创建并启动 MCP 服务 ${name},发现 ${toolCount} 个工具`, toolsChanged: true };
231
+ } catch (e: unknown) {
232
+ const msg = e instanceof Error ? e.message : String(e);
233
+ return { text: `已创建 MCP 服务 ${name},但启动失败: ${msg}`, toolsChanged: false };
234
+ }
235
+ }
236
+
237
+ private async doDelete(args: Args): Promise<{ text: string; toolsChanged: boolean }> {
238
+ const name = args.name as string;
239
+ if (!name) throw new Error("name 必填");
240
+ const hadSession = this.sessions.has(name);
241
+ if (hadSession) await this.stopServer(name);
242
+ this.configManager.delete(name);
243
+ await this.configManager.save();
244
+ return { text: `已删除 MCP 服务 ${name}`, toolsChanged: hadSession };
245
+ }
246
+
247
+ private async doStart(args: Args): Promise<{ text: string; toolsChanged: boolean }> {
248
+ const name = args.name as string;
249
+ if (!name) throw new Error("name 必填");
250
+ const cfg = this.configManager.get(name);
251
+ if (!cfg) throw new Error(`MCP 服务不存在: ${name}`);
252
+ cfg.enabled = true;
253
+ this.configManager.set(name, cfg);
254
+ await this.configManager.save();
255
+ await this.startServer(name, cfg);
256
+ const session = this.sessions.get(name);
257
+ const toolCount = session?.tools.length ?? 0;
258
+ return { text: `已启动 MCP 服务 ${name},发现 ${toolCount} 个工具`, toolsChanged: true };
259
+ }
260
+
261
+ private async doStop(args: Args): Promise<{ text: string; toolsChanged: boolean }> {
262
+ const name = args.name as string;
263
+ if (!name) throw new Error("name 必填");
264
+ const cfg = this.configManager.get(name);
265
+ if (!cfg) throw new Error(`MCP 服务不存在: ${name}`);
266
+ cfg.enabled = false;
267
+ this.configManager.set(name, cfg);
268
+ await this.configManager.save();
269
+ const hadSession = this.sessions.has(name);
270
+ await this.stopServer(name);
271
+ return { text: `已停止 MCP 服务 ${name}`, toolsChanged: hadSession };
272
+ }
273
+ }