cicy-desktop 1.0.8
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/.github/workflows/build.yml +85 -0
- package/.kiro/steering/dev-workflow.md +166 -0
- package/AGENTS.md +247 -0
- package/CLAUDE.md +162 -0
- package/DOCKER.md +85 -0
- package/Dockerfile +46 -0
- package/README.md +720 -0
- package/TODO-anti-detection.md +326 -0
- package/bin/cicy +176 -0
- package/bin/preinstall.sh +32 -0
- package/copy-to-desktop.sh +26 -0
- package/docs/AUTOMATION-API.md +342 -0
- package/docs/REQUEST_MONITORING.md +435 -0
- package/docs/REST-API-FEATURE.md +155 -0
- package/docs/REST-API.md +319 -0
- package/docs/feature-distributed-multi-agent.md +555 -0
- package/docs/yaml.md +255 -0
- package/electron-mcp-fixed.command +134 -0
- package/electron-mcp-simple.command +135 -0
- package/electron-mcp.command +92 -0
- package/generate-openapi.js +158 -0
- package/jest.config.js +10 -0
- package/jest.setup.global.js +13 -0
- package/jest.teardown.global.js +7 -0
- package/package.json +75 -0
- package/service.sh +164 -0
- package/src/config.js +8 -0
- package/src/extension/inject.js +135 -0
- package/src/main-old.js +837 -0
- package/src/main.js +403 -0
- package/src/preload-rpc.js +4 -0
- package/src/server/args-parser.js +37 -0
- package/src/server/electron-setup.js +33 -0
- package/src/server/express-app.js +166 -0
- package/src/server/logging.js +58 -0
- package/src/server/mcp-server.js +53 -0
- package/src/server/tool-registry.js +77 -0
- package/src/server/ui-routes.js +81 -0
- package/src/swagger-ui.html +41 -0
- package/src/tools/account-tools.js +194 -0
- package/src/tools/automation-tools.js +297 -0
- package/src/tools/cdp-tools.js +444 -0
- package/src/tools/clipboard-tools.js +180 -0
- package/src/tools/download-tools.js +57 -0
- package/src/tools/exec-js.js +297 -0
- package/src/tools/exec-tools.js +139 -0
- package/src/tools/file-tools.js +212 -0
- package/src/tools/hook-chatgpt.js +489 -0
- package/src/tools/hook-gemini.js +454 -0
- package/src/tools/index.js +19 -0
- package/src/tools/ipc-bridge.js +31 -0
- package/src/tools/ping.js +60 -0
- package/src/tools/r-reset.js +28 -0
- package/src/tools/screenshot-tools.js +28 -0
- package/src/tools/system-tools.js +531 -0
- package/src/tools/window-tools.js +882 -0
- package/src/ui.html +914 -0
- package/src/utils/auth.js +81 -0
- package/src/utils/cdp-utils.js +8 -0
- package/src/utils/download-manager.js +41 -0
- package/src/utils/process-utils.js +185 -0
- package/src/utils/snapshot-utils.js +56 -0
- package/src/utils/window-monitor.js +605 -0
- package/src/utils/window-state.js +137 -0
- package/src/utils/window-utils.js +336 -0
- package/update-desktop.sh +33 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const { BrowserWindow } = require("electron");
|
|
2
|
+
const { z } = require("zod");
|
|
3
|
+
|
|
4
|
+
function registerTools(registerTool) {
|
|
5
|
+
registerTool(
|
|
6
|
+
"exec_js",
|
|
7
|
+
"在窗口中执行JS代码并返回结果,支持async/await。示例: document.title 或 return document.title 或 await fetch('/api').then(r=>r.json())",
|
|
8
|
+
z.object({
|
|
9
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
10
|
+
code: z.string().describe("JS 代码"),
|
|
11
|
+
}),
|
|
12
|
+
async ({ win_id, code }) => {
|
|
13
|
+
try {
|
|
14
|
+
const win = BrowserWindow.fromId(win_id);
|
|
15
|
+
if (!win) throw new Error(`未找到窗口 ${win_id}`);
|
|
16
|
+
|
|
17
|
+
// 简化代码包装逻辑
|
|
18
|
+
let wrappedCode = code.trim();
|
|
19
|
+
|
|
20
|
+
// 如果代码不包含 return 且不是多语句,自动添加 return
|
|
21
|
+
if (
|
|
22
|
+
!wrappedCode.includes("return") &&
|
|
23
|
+
!wrappedCode.includes(";") &&
|
|
24
|
+
!wrappedCode.includes("\n")
|
|
25
|
+
) {
|
|
26
|
+
wrappedCode = `return ${wrappedCode}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 包装为异步函数
|
|
30
|
+
wrappedCode = `(async () => { ${wrappedCode} })()`;
|
|
31
|
+
|
|
32
|
+
const result = await win.webContents.executeJavaScript(wrappedCode);
|
|
33
|
+
const text =
|
|
34
|
+
result !== null && typeof result === "object"
|
|
35
|
+
? JSON.stringify(result, null, 2)
|
|
36
|
+
: String(result);
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{ type: "text", text },
|
|
40
|
+
{ type: "text", text: `>> exec_js ok` },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{ tag: "JavaScript" }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
registerTool(
|
|
51
|
+
"exec_js_file",
|
|
52
|
+
"Execute JS file in browser window. Provide file path or content.",
|
|
53
|
+
z.object({
|
|
54
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
55
|
+
file: z.string().optional().describe("本地 JS 文件路径"),
|
|
56
|
+
content: z.string().optional().describe("JS content (saved to temp and executed)"),
|
|
57
|
+
}),
|
|
58
|
+
async ({ win_id, file, content: jsContent }) => {
|
|
59
|
+
try {
|
|
60
|
+
const fs = require("fs");
|
|
61
|
+
const path = require("path");
|
|
62
|
+
const os = require("os");
|
|
63
|
+
let code;
|
|
64
|
+
if (jsContent) {
|
|
65
|
+
const tmp = path.join(os.homedir(), "tmp", "_exec_js_" + Date.now() + ".js");
|
|
66
|
+
fs.mkdirSync(path.dirname(tmp), { recursive: true });
|
|
67
|
+
fs.writeFileSync(tmp, jsContent, "utf-8");
|
|
68
|
+
code = jsContent;
|
|
69
|
+
} else if (file) {
|
|
70
|
+
const resolved = path.resolve(file);
|
|
71
|
+
if (!fs.existsSync(resolved)) throw new Error("文件不存在: " + resolved);
|
|
72
|
+
code = fs.readFileSync(resolved, "utf-8");
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error("需要 file 或 content 参数");
|
|
75
|
+
}
|
|
76
|
+
const win = BrowserWindow.fromId(win_id);
|
|
77
|
+
if (!win) throw new Error("未找到窗口 " + win_id);
|
|
78
|
+
const wrappedCode = "(async () => { " + code + " })()";
|
|
79
|
+
const result = await win.webContents.executeJavaScript(wrappedCode);
|
|
80
|
+
const text = result !== null && typeof result === "object" ? JSON.stringify(result, null, 2) : String(result);
|
|
81
|
+
return { content: [{ type: "text", text }, { type: "text", text: ">> exec_js_file ok" }] };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{ tag: "JavaScript" }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
registerTool(
|
|
91
|
+
"get_element_client_bound",
|
|
92
|
+
"获取页面元素的位置和尺寸信息(getBoundingClientRect)。返回元素的 x, y, width, height, top, left, right, bottom 坐标。selector 必须唯一且不能为空",
|
|
93
|
+
z.object({
|
|
94
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
95
|
+
selector: z
|
|
96
|
+
.string()
|
|
97
|
+
.min(1, "CSS选择器不能为空")
|
|
98
|
+
.describe("CSS 选择器,如 '#id', '.class', 'button'"),
|
|
99
|
+
}),
|
|
100
|
+
async ({ win_id, selector }) => {
|
|
101
|
+
try {
|
|
102
|
+
const win = BrowserWindow.fromId(win_id);
|
|
103
|
+
if (!win) throw new Error(`未找到窗口 ${win_id}`);
|
|
104
|
+
|
|
105
|
+
const escapedSelector = selector.replace(/'/g, "\\'");
|
|
106
|
+
const result = await win.webContents.executeJavaScript(`
|
|
107
|
+
(function() {
|
|
108
|
+
const selector = '${escapedSelector}';
|
|
109
|
+
|
|
110
|
+
if (!selector || selector.trim() === '') {
|
|
111
|
+
return { error: 'CSS选择器不能为空' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const allElements = document.querySelectorAll(selector);
|
|
115
|
+
if (allElements.length === 0) {
|
|
116
|
+
return { error: '未找到匹配的元素: ' + selector };
|
|
117
|
+
}
|
|
118
|
+
if (allElements.length > 1) {
|
|
119
|
+
return { error: '选择器匹配多个元素,请使用唯一选择器,匹配数量: ' + allElements.length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const el = allElements[0];
|
|
123
|
+
const rect = el.getBoundingClientRect();
|
|
124
|
+
return {
|
|
125
|
+
selector: selector,
|
|
126
|
+
count: allElements.length,
|
|
127
|
+
x: Math.round(rect.x),
|
|
128
|
+
y: Math.round(rect.y),
|
|
129
|
+
width: Math.round(rect.width),
|
|
130
|
+
height: Math.round(rect.height),
|
|
131
|
+
top: Math.round(rect.top),
|
|
132
|
+
left: Math.round(rect.left),
|
|
133
|
+
right: Math.round(rect.right),
|
|
134
|
+
bottom: Math.round(rect.bottom)
|
|
135
|
+
};
|
|
136
|
+
})()
|
|
137
|
+
`);
|
|
138
|
+
|
|
139
|
+
if (result.error) {
|
|
140
|
+
return { content: [{ type: "text", text: result.error }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{ tag: "JavaScript" }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
registerTool(
|
|
152
|
+
"show_float_div",
|
|
153
|
+
"在页面中心显示一个可拖拽、可调整大小的浮动 div。支持自定义位置、尺寸和透明度。",
|
|
154
|
+
z.object({
|
|
155
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
156
|
+
x: z.number().optional().describe("X 坐标 (默认居中)"),
|
|
157
|
+
y: z.number().optional().describe("Y 坐标 (默认居中)"),
|
|
158
|
+
w: z.number().optional().default(300).describe("宽度 (默认 300)"),
|
|
159
|
+
h: z.number().optional().default(200).describe("高度 (默认 200)"),
|
|
160
|
+
opacity: z.number().optional().default(0.7).describe("背景透明度 0-1 (默认 0.7)"),
|
|
161
|
+
}),
|
|
162
|
+
async ({ win_id, x, y, w, h, opacity }) => {
|
|
163
|
+
try {
|
|
164
|
+
const win = BrowserWindow.fromId(win_id);
|
|
165
|
+
if (!win) throw new Error(`未找到窗口 ${win_id}`);
|
|
166
|
+
|
|
167
|
+
const jsCode = `(function() {
|
|
168
|
+
try {
|
|
169
|
+
var existDiv = document.getElementById('__float_div__');
|
|
170
|
+
if (existDiv) { existDiv.remove(); }
|
|
171
|
+
|
|
172
|
+
var div = document.createElement('div');
|
|
173
|
+
div.id = '__float_div__';
|
|
174
|
+
|
|
175
|
+
var viewportW = window.innerWidth;
|
|
176
|
+
var viewportH = window.innerHeight;
|
|
177
|
+
var initW = ${w} || 300;
|
|
178
|
+
var initH = ${h} || 200;
|
|
179
|
+
var initX = ${x} || Math.round((viewportW - initW) / 2);
|
|
180
|
+
var initY = ${y} || Math.round((viewportH - initH) / 2);
|
|
181
|
+
var opacity = ${opacity} || 0.7;
|
|
182
|
+
|
|
183
|
+
div.style.cssText = 'position:fixed;left:' + initX + 'px;top:' + initY + 'px;width:' + initW + 'px;height:' + initH + 'px;background:rgba(255,0,0,' + opacity + ');color:#fff;z-index:999999;border:2px solid #fff;';
|
|
184
|
+
|
|
185
|
+
// 左上角坐标
|
|
186
|
+
var tl = document.createElement('div');
|
|
187
|
+
tl.textContent = initX + ',' + initY;
|
|
188
|
+
tl.style.cssText = 'position:absolute;top:4px;left:4px;font-family:monospace;font-size:14px;font-weight:bold;color:#fff;';
|
|
189
|
+
div.appendChild(tl);
|
|
190
|
+
|
|
191
|
+
// 右上角坐标
|
|
192
|
+
var tr = document.createElement('div');
|
|
193
|
+
tr.textContent = (initX + initW) + ',' + initY;
|
|
194
|
+
tr.style.cssText = 'position:absolute;top:4px;right:4px;font-family:monospace;font-size:14px;font-weight:bold;color:#fff;';
|
|
195
|
+
div.appendChild(tr);
|
|
196
|
+
|
|
197
|
+
// 左下角坐标
|
|
198
|
+
var bl = document.createElement('div');
|
|
199
|
+
bl.textContent = initX + ',' + (initY + initH);
|
|
200
|
+
bl.style.cssText = 'position:absolute;bottom:4px;left:4px;font-family:monospace;font-size:14px;font-weight:bold;color:#fff;';
|
|
201
|
+
div.appendChild(bl);
|
|
202
|
+
|
|
203
|
+
// 右下角坐标
|
|
204
|
+
var br = document.createElement('div');
|
|
205
|
+
br.textContent = (initX + initW) + ',' + (initY + initH);
|
|
206
|
+
br.style.cssText = 'position:absolute;bottom:4px;right:4px;font-family:monospace;font-size:14px;font-weight:bold;color:#fff;';
|
|
207
|
+
div.appendChild(br);
|
|
208
|
+
|
|
209
|
+
// 关闭按钮
|
|
210
|
+
var closeBtn = document.createElement('div');
|
|
211
|
+
closeBtn.textContent = 'X';
|
|
212
|
+
closeBtn.style.cssText = 'position:absolute;top:50%;right:4px;transform:translateY(-50%);width:16px;height:16px;cursor:pointer;font-size:12px;font-weight:bold;background:#fff;color:#f00;text-align:center;line-height:16px;';
|
|
213
|
+
closeBtn.onclick = function(e) { e.stopPropagation(); div.remove(); };
|
|
214
|
+
div.appendChild(closeBtn);
|
|
215
|
+
|
|
216
|
+
// resize 把手
|
|
217
|
+
var resizeHandle = document.createElement('div');
|
|
218
|
+
resizeHandle.style.cssText = 'position:absolute;right:0;bottom:0;width:14px;height:14px;background:#fff;cursor:se-resize;';
|
|
219
|
+
div.appendChild(resizeHandle);
|
|
220
|
+
|
|
221
|
+
var dragX = initX, dragY = initY, curW = initW, curH = initH;
|
|
222
|
+
var isDrag = false, isResize = false, startX, startY;
|
|
223
|
+
|
|
224
|
+
div.addEventListener('mousedown', function(e) {
|
|
225
|
+
if (e.target === closeBtn || e.target === resizeHandle) return;
|
|
226
|
+
isDrag = true;
|
|
227
|
+
startX = e.clientX;
|
|
228
|
+
startY = e.clientY;
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
e.stopPropagation();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
resizeHandle.addEventListener('mousedown', function(e) {
|
|
234
|
+
isResize = true;
|
|
235
|
+
startX = e.clientX;
|
|
236
|
+
startY = e.clientY;
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
e.stopPropagation();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
document.addEventListener('mousemove', function(e) {
|
|
242
|
+
if (isDrag) {
|
|
243
|
+
dragX += e.clientX - startX;
|
|
244
|
+
dragY += e.clientY - startY;
|
|
245
|
+
div.style.left = dragX + 'px';
|
|
246
|
+
div.style.top = dragY + 'px';
|
|
247
|
+
startX = e.clientX;
|
|
248
|
+
startY = e.clientY;
|
|
249
|
+
tl.textContent = Math.round(dragX) + ',' + Math.round(dragY);
|
|
250
|
+
tr.textContent = Math.round(dragX + curW) + ',' + Math.round(dragY);
|
|
251
|
+
bl.textContent = Math.round(dragX) + ',' + Math.round(dragY + curH);
|
|
252
|
+
br.textContent = Math.round(dragX + curW) + ',' + Math.round(dragY + curH);
|
|
253
|
+
} else if (isResize) {
|
|
254
|
+
curW = Math.max(100, curW + e.clientX - startX);
|
|
255
|
+
curH = Math.max(60, curH + e.clientY - startY);
|
|
256
|
+
div.style.width = curW + 'px';
|
|
257
|
+
div.style.height = curH + 'px';
|
|
258
|
+
startX = e.clientX;
|
|
259
|
+
startY = e.clientY;
|
|
260
|
+
tr.textContent = Math.round(dragX + curW) + ',' + Math.round(dragY);
|
|
261
|
+
bl.textContent = Math.round(dragX) + ',' + Math.round(dragY + curH);
|
|
262
|
+
br.textContent = Math.round(dragX + curW) + ',' + Math.round(dragY + curH);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
document.addEventListener('mouseup', function() {
|
|
267
|
+
isDrag = false;
|
|
268
|
+
isResize = false;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
document.body.appendChild(div);
|
|
272
|
+
return 'OK: div at ' + Math.round(initX) + ',' + Math.round(initY) + ' size ' + initW + 'x' + initH;
|
|
273
|
+
} catch(e) { return 'ERROR: ' + e.message; }
|
|
274
|
+
})()`;
|
|
275
|
+
|
|
276
|
+
const result = await win.webContents.debugger.sendCommand("Runtime.evaluate", {
|
|
277
|
+
expression: jsCode,
|
|
278
|
+
});
|
|
279
|
+
console.log("[show_float_div] result:", result);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: result.result?.value || result.result?.description || JSON.stringify(result),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{ tag: "JavaScript" }
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = registerTools;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
const { exec } = require("child_process");
|
|
3
|
+
const util = require("util");
|
|
4
|
+
const execPromise = util.promisify(exec);
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
|
|
9
|
+
const TMP = path.join(os.homedir(), "tmp");
|
|
10
|
+
if (!fs.existsSync(TMP)) fs.mkdirSync(TMP, { recursive: true });
|
|
11
|
+
|
|
12
|
+
function writeTemp(name, content) {
|
|
13
|
+
const p = path.join(TMP, name);
|
|
14
|
+
fs.writeFileSync(p, content, "utf-8");
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveFile(file, content, ext) {
|
|
19
|
+
if (content) return writeTemp(`_exec_${Date.now()}${ext}`, content);
|
|
20
|
+
const resolved = path.resolve(file);
|
|
21
|
+
if (!fs.existsSync(resolved)) throw new Error("File not found: " + resolved);
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function result(stdout, stderr) {
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify({ stdout: stdout || "", stderr: stderr || "", exitCode: 0 }, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
function errorResult(error) {
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify({ stdout: error.stdout || "", stderr: error.stderr || error.message, exitCode: error.code || 1 }, null, 2) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function registerTools(registerTool) {
|
|
33
|
+
registerTool(
|
|
34
|
+
"exec_shell",
|
|
35
|
+
"Execute shell command",
|
|
36
|
+
z.object({
|
|
37
|
+
command: z.string().describe("Shell command to execute"),
|
|
38
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
39
|
+
}),
|
|
40
|
+
async ({ command, cwd }) => {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout, stderr } = await execPromise(command, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
43
|
+
return result(stdout, stderr);
|
|
44
|
+
} catch (e) { return errorResult(e); }
|
|
45
|
+
},
|
|
46
|
+
{ tag: "Exec" }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
registerTool(
|
|
50
|
+
"exec_python",
|
|
51
|
+
"Execute Python code",
|
|
52
|
+
z.object({
|
|
53
|
+
code: z.string().describe("Python code to execute"),
|
|
54
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
55
|
+
}),
|
|
56
|
+
async ({ code, cwd }) => {
|
|
57
|
+
try {
|
|
58
|
+
const py = process.platform === "win32" ? "python" : "python3";
|
|
59
|
+
const { stdout, stderr } = await execPromise(`${py} -c ${JSON.stringify(code)}`, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
60
|
+
return result(stdout, stderr);
|
|
61
|
+
} catch (e) { return errorResult(e); }
|
|
62
|
+
},
|
|
63
|
+
{ tag: "Exec" }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
registerTool(
|
|
67
|
+
"exec_node",
|
|
68
|
+
"Execute Node.js code",
|
|
69
|
+
z.object({
|
|
70
|
+
code: z.string().describe("Node.js code to execute"),
|
|
71
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
72
|
+
}),
|
|
73
|
+
async ({ code, cwd }) => {
|
|
74
|
+
try {
|
|
75
|
+
const { stdout, stderr } = await execPromise(`node -e ${JSON.stringify(code)}`, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
76
|
+
return result(stdout, stderr);
|
|
77
|
+
} catch (e) { return errorResult(e); }
|
|
78
|
+
},
|
|
79
|
+
{ tag: "Exec" }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
registerTool(
|
|
83
|
+
"exec_shell_file",
|
|
84
|
+
"Execute shell script. Provide file path or content (content will be saved to temp file and executed).",
|
|
85
|
+
z.object({
|
|
86
|
+
file: z.string().optional().describe("Path to shell script file"),
|
|
87
|
+
content: z.string().optional().describe("Shell script content (uploaded and executed)"),
|
|
88
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
89
|
+
}),
|
|
90
|
+
async ({ file, content, cwd }) => {
|
|
91
|
+
try {
|
|
92
|
+
const resolved = resolveFile(file, content, ".bat");
|
|
93
|
+
const cmd = process.platform === "win32" ? `"${resolved}"` : `bash "${resolved}"`;
|
|
94
|
+
const { stdout, stderr } = await execPromise(cmd, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
95
|
+
return result(stdout, stderr);
|
|
96
|
+
} catch (e) { return errorResult(e); }
|
|
97
|
+
},
|
|
98
|
+
{ tag: "Exec" }
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
registerTool(
|
|
102
|
+
"exec_python_file",
|
|
103
|
+
"Execute Python script. Provide file path or content (content will be saved to temp file and executed).",
|
|
104
|
+
z.object({
|
|
105
|
+
file: z.string().optional().describe("Path to Python script file"),
|
|
106
|
+
content: z.string().optional().describe("Python script content (uploaded and executed)"),
|
|
107
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
108
|
+
}),
|
|
109
|
+
async ({ file, content, cwd }) => {
|
|
110
|
+
try {
|
|
111
|
+
const resolved = resolveFile(file, content, ".py");
|
|
112
|
+
const py = process.platform === "win32" ? "python" : "python3";
|
|
113
|
+
const { stdout, stderr } = await execPromise(`${py} "${resolved}"`, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
114
|
+
return result(stdout, stderr);
|
|
115
|
+
} catch (e) { return errorResult(e); }
|
|
116
|
+
},
|
|
117
|
+
{ tag: "Exec" }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
registerTool(
|
|
121
|
+
"exec_node_file",
|
|
122
|
+
"Execute Node.js script. Provide file path or content (content will be saved to temp file and executed).",
|
|
123
|
+
z.object({
|
|
124
|
+
file: z.string().optional().describe("Path to Node.js script file"),
|
|
125
|
+
content: z.string().optional().describe("Node.js script content (uploaded and executed)"),
|
|
126
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
127
|
+
}),
|
|
128
|
+
async ({ file, content, cwd }) => {
|
|
129
|
+
try {
|
|
130
|
+
const resolved = resolveFile(file, content, ".js");
|
|
131
|
+
const { stdout, stderr } = await execPromise(`node "${resolved}"`, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
132
|
+
return result(stdout, stderr);
|
|
133
|
+
} catch (e) { return errorResult(e); }
|
|
134
|
+
},
|
|
135
|
+
{ tag: "Exec" }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = registerTools;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
|
|
6
|
+
const FILES_DIR = path.join(require("os").homedir(), "electron-mcp-files");
|
|
7
|
+
|
|
8
|
+
function registerTools(registerTool) {
|
|
9
|
+
// 文本文件工具(默认 UTF-8)
|
|
10
|
+
registerTool(
|
|
11
|
+
"file_read",
|
|
12
|
+
"读取文本文件内容",
|
|
13
|
+
z.object({
|
|
14
|
+
path: z.string().describe("文件路径"),
|
|
15
|
+
}),
|
|
16
|
+
async ({ path: filePath }) => {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
throw new Error(`File not found: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
23
|
+
const stats = fs.statSync(filePath);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
path: filePath,
|
|
32
|
+
size: stats.size,
|
|
33
|
+
content,
|
|
34
|
+
},
|
|
35
|
+
null,
|
|
36
|
+
2
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ tag: "File" }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
registerTool(
|
|
52
|
+
"file_write",
|
|
53
|
+
"写入文本文件内容",
|
|
54
|
+
z.object({
|
|
55
|
+
path: z.string().describe("文件路径"),
|
|
56
|
+
content: z.string().describe("文件内容"),
|
|
57
|
+
}),
|
|
58
|
+
async ({ path: filePath, content }) => {
|
|
59
|
+
try {
|
|
60
|
+
const dir = path.dirname(filePath);
|
|
61
|
+
if (!fs.existsSync(dir)) {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
66
|
+
const stats = fs.statSync(filePath);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify(
|
|
73
|
+
{
|
|
74
|
+
success: true,
|
|
75
|
+
path: filePath,
|
|
76
|
+
size: stats.size,
|
|
77
|
+
message: `File written: ${stats.size} bytes`,
|
|
78
|
+
},
|
|
79
|
+
null,
|
|
80
|
+
2
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{ tag: "File" }
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// 文件上传下载(通过 HTTP)
|
|
96
|
+
registerTool(
|
|
97
|
+
"file_upload",
|
|
98
|
+
"上传文件到服务器(base64编码),返回可访问的 URL",
|
|
99
|
+
z.object({
|
|
100
|
+
filename: z.string().describe("文件名"),
|
|
101
|
+
content: z.string().describe("文件内容(base64编码)"),
|
|
102
|
+
}),
|
|
103
|
+
async ({ filename, content }) => {
|
|
104
|
+
try {
|
|
105
|
+
if (!fs.existsSync(FILES_DIR)) {
|
|
106
|
+
fs.mkdirSync(FILES_DIR, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const hash = crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
|
|
110
|
+
const ext = path.extname(filename);
|
|
111
|
+
const basename = path.basename(filename, ext);
|
|
112
|
+
const uniqueFilename = `${basename}-${hash}${ext}`;
|
|
113
|
+
const filePath = path.join(FILES_DIR, uniqueFilename);
|
|
114
|
+
|
|
115
|
+
const buffer = Buffer.from(content, "base64");
|
|
116
|
+
fs.writeFileSync(filePath, buffer);
|
|
117
|
+
const stats = fs.statSync(filePath);
|
|
118
|
+
|
|
119
|
+
const baseUrl =
|
|
120
|
+
process.env.ELECTRON_MCP_BASE_URL || `http://localhost:${process.env.PORT || 8101}`;
|
|
121
|
+
const url = `${baseUrl}/files/${uniqueFilename}`;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: JSON.stringify(
|
|
128
|
+
{
|
|
129
|
+
success: true,
|
|
130
|
+
filename: uniqueFilename,
|
|
131
|
+
path: filePath,
|
|
132
|
+
url,
|
|
133
|
+
size: stats.size,
|
|
134
|
+
message: `File uploaded: ${stats.size} bytes`,
|
|
135
|
+
},
|
|
136
|
+
null,
|
|
137
|
+
2
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
145
|
+
isError: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{ tag: "File" }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
registerTool(
|
|
153
|
+
"file_download",
|
|
154
|
+
"从本地路径下载文件,复制到服务器并返回 URL",
|
|
155
|
+
z.object({
|
|
156
|
+
path: z.string().describe("本地文件路径"),
|
|
157
|
+
}),
|
|
158
|
+
async ({ path: filePath }) => {
|
|
159
|
+
try {
|
|
160
|
+
if (!fs.existsSync(filePath)) {
|
|
161
|
+
throw new Error(`File not found: ${filePath}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!fs.existsSync(FILES_DIR)) {
|
|
165
|
+
fs.mkdirSync(FILES_DIR, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const stats = fs.statSync(filePath);
|
|
169
|
+
const buffer = fs.readFileSync(filePath);
|
|
170
|
+
const hash = crypto.createHash("md5").update(buffer).digest("hex").slice(0, 8);
|
|
171
|
+
const ext = path.extname(filePath);
|
|
172
|
+
const basename = path.basename(filePath, ext);
|
|
173
|
+
const uniqueFilename = `${basename}-${hash}${ext}`;
|
|
174
|
+
const destPath = path.join(FILES_DIR, uniqueFilename);
|
|
175
|
+
|
|
176
|
+
fs.copyFileSync(filePath, destPath);
|
|
177
|
+
|
|
178
|
+
const baseUrl =
|
|
179
|
+
process.env.ELECTRON_MCP_BASE_URL || `http://localhost:${process.env.PORT || 8101}`;
|
|
180
|
+
const url = `${baseUrl}/files/${uniqueFilename}`;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: JSON.stringify(
|
|
187
|
+
{
|
|
188
|
+
success: true,
|
|
189
|
+
filename: uniqueFilename,
|
|
190
|
+
path: destPath,
|
|
191
|
+
url,
|
|
192
|
+
size: stats.size,
|
|
193
|
+
message: `File copied: ${stats.size} bytes`,
|
|
194
|
+
},
|
|
195
|
+
null,
|
|
196
|
+
2
|
|
197
|
+
),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{ tag: "File" }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = registerTools;
|