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