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,81 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const log = require("electron-log");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 认证模块 - 处理令牌生成、验证和管理
|
|
9
|
+
*/
|
|
10
|
+
class AuthManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.authToken = this.getOrGenerateToken();
|
|
13
|
+
log.info("[MCP] Auth token enabled");
|
|
14
|
+
log.info("[MCP] Token saved to ~/data/electron/token.txt");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 获取或生成认证令牌
|
|
19
|
+
* @returns {string} 认证令牌
|
|
20
|
+
*/
|
|
21
|
+
getOrGenerateToken() {
|
|
22
|
+
const tokenPath = path.join(os.homedir(), "data", "electron", "token.txt");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// 检查是否已存在令牌
|
|
26
|
+
if (fs.existsSync(tokenPath)) {
|
|
27
|
+
const token = fs.readFileSync(tokenPath, "utf8").trim();
|
|
28
|
+
if (token) {
|
|
29
|
+
log.info("[MCP] Using existing token from", tokenPath);
|
|
30
|
+
return token;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 生成新令牌
|
|
35
|
+
const newToken = crypto.randomBytes(32).toString("hex");
|
|
36
|
+
fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
|
|
37
|
+
fs.writeFileSync(tokenPath, newToken);
|
|
38
|
+
log.info("[MCP] Generated new token and saved to", tokenPath);
|
|
39
|
+
return newToken;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
log.error("[MCP] Token management error:", error);
|
|
42
|
+
return crypto.randomBytes(32).toString("hex"); // fallback
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 验证认证令牌(支持 Bearer 和 Basic Auth)
|
|
48
|
+
* @param {Object} req - HTTP 请求对象
|
|
49
|
+
* @returns {boolean} 验证结果
|
|
50
|
+
*/
|
|
51
|
+
validateAuth(req) {
|
|
52
|
+
const authHeader = req.headers.authorization;
|
|
53
|
+
if (!authHeader) return false;
|
|
54
|
+
|
|
55
|
+
// Bearer token
|
|
56
|
+
if (authHeader.startsWith("Bearer ")) {
|
|
57
|
+
const token = authHeader.replace("Bearer ", "");
|
|
58
|
+
return token === this.authToken;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Basic Auth (username:password where password is token)
|
|
62
|
+
if (authHeader.startsWith("Basic ")) {
|
|
63
|
+
const base64Credentials = authHeader.replace("Basic ", "");
|
|
64
|
+
const credentials = Buffer.from(base64Credentials, "base64").toString("utf8");
|
|
65
|
+
const [, password] = credentials.split(":");
|
|
66
|
+
return password === this.authToken;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 获取当前认证令牌
|
|
74
|
+
* @returns {string} 当前令牌
|
|
75
|
+
*/
|
|
76
|
+
getToken() {
|
|
77
|
+
return this.authToken;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { AuthManager };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 全局下载管理器
|
|
2
|
+
const downloads = new Map();
|
|
3
|
+
let downloadIdCounter = 0;
|
|
4
|
+
|
|
5
|
+
function addDownload(downloadInfo) {
|
|
6
|
+
const id = ++downloadIdCounter;
|
|
7
|
+
downloads.set(id, {
|
|
8
|
+
id,
|
|
9
|
+
...downloadInfo,
|
|
10
|
+
createdAt: new Date().toISOString(),
|
|
11
|
+
});
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function updateDownload(id, updates) {
|
|
16
|
+
const download = downloads.get(id);
|
|
17
|
+
if (download) {
|
|
18
|
+
Object.assign(download, updates, { updatedAt: new Date().toISOString() });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getDownload(id) {
|
|
23
|
+
return downloads.get(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getAllDownloads() {
|
|
27
|
+
return Array.from(downloads.values());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearDownloads() {
|
|
31
|
+
downloads.clear();
|
|
32
|
+
downloadIdCounter = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
addDownload,
|
|
37
|
+
updateDownload,
|
|
38
|
+
getDownload,
|
|
39
|
+
getAllDownloads,
|
|
40
|
+
clearDownloads,
|
|
41
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const { exec } = require("child_process");
|
|
2
|
+
const net = require("net");
|
|
3
|
+
const { promisify } = require("util");
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 检查端口是否开放(被占用)
|
|
9
|
+
* @param {number} port - 端口号
|
|
10
|
+
* @param {string} [host='localhost'] - 主机地址
|
|
11
|
+
* @param {number} [timeout=1000] - 超时时间(毫秒)
|
|
12
|
+
* @returns {Promise<boolean>} true=端口开放/被占用, false=端口关闭/可用
|
|
13
|
+
*/
|
|
14
|
+
async function isPortOpen(port, host = "localhost", timeout = 1000) {
|
|
15
|
+
// 验证端口范围
|
|
16
|
+
if (typeof port !== "number" || port < 0 || port >= 65536) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const socket = new net.Socket();
|
|
22
|
+
let isResolved = false;
|
|
23
|
+
|
|
24
|
+
const onError = () => {
|
|
25
|
+
if (!isResolved) {
|
|
26
|
+
isResolved = true;
|
|
27
|
+
socket.destroy();
|
|
28
|
+
resolve(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const onTimeout = () => {
|
|
33
|
+
if (!isResolved) {
|
|
34
|
+
isResolved = true;
|
|
35
|
+
socket.destroy();
|
|
36
|
+
resolve(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const onConnect = () => {
|
|
41
|
+
if (!isResolved) {
|
|
42
|
+
isResolved = true;
|
|
43
|
+
socket.destroy();
|
|
44
|
+
resolve(true);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
socket.setTimeout(timeout);
|
|
49
|
+
socket.once("error", onError);
|
|
50
|
+
socket.once("timeout", onTimeout);
|
|
51
|
+
socket.once("connect", onConnect);
|
|
52
|
+
|
|
53
|
+
socket.connect(port, host);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 杀死占用指定端口的进程
|
|
59
|
+
* @param {number} port - 端口号
|
|
60
|
+
* @returns {Promise<{success: boolean, message: string, pid?: number}>}
|
|
61
|
+
*/
|
|
62
|
+
async function killPort(port) {
|
|
63
|
+
const platform = process.platform;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (platform === "win32") {
|
|
67
|
+
return await killPortWindows(port);
|
|
68
|
+
} else {
|
|
69
|
+
return await killPortUnix(port);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
message: `Failed to kill process on port ${port}: ${error.message}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Windows 平台杀死端口进程
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
async function killPortWindows(port) {
|
|
84
|
+
try {
|
|
85
|
+
// 查找占用端口的进程
|
|
86
|
+
const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
|
|
87
|
+
|
|
88
|
+
if (!stdout.trim()) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
message: `No process found on port ${port}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 提取 PID(最后一列)
|
|
96
|
+
const lines = stdout.trim().split("\n");
|
|
97
|
+
const pids = new Set();
|
|
98
|
+
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
const parts = line.trim().split(/\s+/);
|
|
101
|
+
const pid = parts[parts.length - 1];
|
|
102
|
+
if (pid && pid !== "0") {
|
|
103
|
+
pids.add(pid);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pids.size === 0) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
message: `No valid PID found for port ${port}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 杀死所有相关进程
|
|
115
|
+
for (const pid of pids) {
|
|
116
|
+
try {
|
|
117
|
+
await execAsync(`taskkill /F /PID ${pid}`);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// 进程可能已经结束,忽略错误
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
message: `Killed process(es) on port ${port}`,
|
|
126
|
+
pid: parseInt(Array.from(pids)[0]),
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
message: `Failed to kill process on port ${port}: ${error.message}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Unix 平台(Linux/macOS)杀死端口进程
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
async function killPortUnix(port) {
|
|
141
|
+
try {
|
|
142
|
+
// 使用 fuser 查找占用端口的进程(不需要 root 权限)
|
|
143
|
+
const { stdout } = await execAsync(`fuser ${port}/tcp 2>/dev/null || true`);
|
|
144
|
+
|
|
145
|
+
if (!stdout.trim()) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
message: `No process found on port ${port}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 提取 PID
|
|
153
|
+
const pids = stdout
|
|
154
|
+
.trim()
|
|
155
|
+
.split(/\s+/)
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((pid) => parseInt(pid));
|
|
158
|
+
|
|
159
|
+
if (pids.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
message: `No valid PID found for port ${port}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 杀死进程
|
|
167
|
+
await execAsync(`kill -9 ${pids.join(" ")}`);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
message: `Killed process(es) on port ${port}`,
|
|
172
|
+
pid: pids[0],
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
message: `Failed to kill process on port ${port}: ${error.message}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
isPortOpen,
|
|
184
|
+
killPort,
|
|
185
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// snapshot-utils.js
|
|
2
|
+
// Snapshot utilities for Electron MCP server
|
|
3
|
+
// Provides page snapshot capture with screenshots and element references
|
|
4
|
+
const { clipboard, nativeImage } = require("electron");
|
|
5
|
+
|
|
6
|
+
async function captureSnapshot(webContents, options = {}) {
|
|
7
|
+
const { win_id = 1 } = options;
|
|
8
|
+
|
|
9
|
+
const image = await webContents.capturePage();
|
|
10
|
+
let size = image.getSize(); // 逻辑尺寸
|
|
11
|
+
|
|
12
|
+
// 检测是否为 macOS
|
|
13
|
+
if (process.platform === "darwin") {
|
|
14
|
+
// 将图片缩小到逻辑尺寸(即原始物理像素的一半)
|
|
15
|
+
// resize 会返回一个新的 NativeImage 对象
|
|
16
|
+
const resizedImage = image.resize({
|
|
17
|
+
width: size.width,
|
|
18
|
+
height: size.height,
|
|
19
|
+
quality: "better", // 可选: 'good', 'better', 'best'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 更新数据
|
|
23
|
+
var finalImage = resizedImage;
|
|
24
|
+
} else {
|
|
25
|
+
var finalImage = image;
|
|
26
|
+
}
|
|
27
|
+
const pngBuffer = finalImage.toPNG();
|
|
28
|
+
// 2. 使用 Buffer 重新创建 NativeImage (确保数据完整)
|
|
29
|
+
const imageToPaste = nativeImage.createFromBuffer(pngBuffer);
|
|
30
|
+
|
|
31
|
+
// 3. 写入剪贴板
|
|
32
|
+
clipboard.writeImage(imageToPaste);
|
|
33
|
+
|
|
34
|
+
const screenshotData = finalImage.toPNG().toString("base64");
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `Size: ${size.width}x${size.height} (macOS auto-scaled)`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `Image has write to clipboard`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "image",
|
|
47
|
+
data: screenshotData,
|
|
48
|
+
mimeType: "image/png",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
captureSnapshot,
|
|
56
|
+
};
|