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.
Files changed (66) hide show
  1. package/.github/workflows/build.yml +85 -0
  2. package/.kiro/steering/dev-workflow.md +166 -0
  3. package/AGENTS.md +247 -0
  4. package/CLAUDE.md +162 -0
  5. package/DOCKER.md +85 -0
  6. package/Dockerfile +46 -0
  7. package/README.md +720 -0
  8. package/TODO-anti-detection.md +326 -0
  9. package/bin/cicy +176 -0
  10. package/bin/preinstall.sh +32 -0
  11. package/copy-to-desktop.sh +26 -0
  12. package/docs/AUTOMATION-API.md +342 -0
  13. package/docs/REQUEST_MONITORING.md +435 -0
  14. package/docs/REST-API-FEATURE.md +155 -0
  15. package/docs/REST-API.md +319 -0
  16. package/docs/feature-distributed-multi-agent.md +555 -0
  17. package/docs/yaml.md +255 -0
  18. package/electron-mcp-fixed.command +134 -0
  19. package/electron-mcp-simple.command +135 -0
  20. package/electron-mcp.command +92 -0
  21. package/generate-openapi.js +158 -0
  22. package/jest.config.js +10 -0
  23. package/jest.setup.global.js +13 -0
  24. package/jest.teardown.global.js +7 -0
  25. package/package.json +75 -0
  26. package/service.sh +164 -0
  27. package/src/config.js +8 -0
  28. package/src/extension/inject.js +135 -0
  29. package/src/main-old.js +837 -0
  30. package/src/main.js +403 -0
  31. package/src/preload-rpc.js +4 -0
  32. package/src/server/args-parser.js +37 -0
  33. package/src/server/electron-setup.js +33 -0
  34. package/src/server/express-app.js +166 -0
  35. package/src/server/logging.js +58 -0
  36. package/src/server/mcp-server.js +53 -0
  37. package/src/server/tool-registry.js +77 -0
  38. package/src/server/ui-routes.js +81 -0
  39. package/src/swagger-ui.html +41 -0
  40. package/src/tools/account-tools.js +194 -0
  41. package/src/tools/automation-tools.js +297 -0
  42. package/src/tools/cdp-tools.js +444 -0
  43. package/src/tools/clipboard-tools.js +180 -0
  44. package/src/tools/download-tools.js +57 -0
  45. package/src/tools/exec-js.js +297 -0
  46. package/src/tools/exec-tools.js +139 -0
  47. package/src/tools/file-tools.js +212 -0
  48. package/src/tools/hook-chatgpt.js +489 -0
  49. package/src/tools/hook-gemini.js +454 -0
  50. package/src/tools/index.js +19 -0
  51. package/src/tools/ipc-bridge.js +31 -0
  52. package/src/tools/ping.js +60 -0
  53. package/src/tools/r-reset.js +28 -0
  54. package/src/tools/screenshot-tools.js +28 -0
  55. package/src/tools/system-tools.js +531 -0
  56. package/src/tools/window-tools.js +882 -0
  57. package/src/ui.html +914 -0
  58. package/src/utils/auth.js +81 -0
  59. package/src/utils/cdp-utils.js +8 -0
  60. package/src/utils/download-manager.js +41 -0
  61. package/src/utils/process-utils.js +185 -0
  62. package/src/utils/snapshot-utils.js +56 -0
  63. package/src/utils/window-monitor.js +605 -0
  64. package/src/utils/window-state.js +137 -0
  65. package/src/utils/window-utils.js +336 -0
  66. package/update-desktop.sh +33 -0
@@ -0,0 +1,137 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const crypto = require("crypto");
5
+ const log = require("electron-log");
6
+
7
+ const STATE_DIR = path.join(os.homedir(), "data", "electron", "state");
8
+
9
+ // 确保状态目录存在
10
+ function ensureStateDir() {
11
+ if (!fs.existsSync(STATE_DIR)) {
12
+ fs.mkdirSync(STATE_DIR, { recursive: true });
13
+ }
14
+ }
15
+
16
+ // 生成URL的hash作为唯一标识
17
+ function getUrlHash(url) {
18
+ if (!url) return "default";
19
+ try {
20
+ const urlObj = new URL(url);
21
+ const domain = urlObj.hostname.replace(/^www\./, "");
22
+ return crypto.createHash("md5").update(domain).digest("hex").substring(0, 8);
23
+ } catch {
24
+ return "default";
25
+ }
26
+ }
27
+
28
+ // 获取状态文件路径
29
+ function getStatePath(accountIdx, urlHash) {
30
+ return path.join(STATE_DIR, `${accountIdx}-${urlHash}.json`);
31
+ }
32
+
33
+ // 加载窗口状态
34
+ function loadWindowState(accountIdx, url) {
35
+ try {
36
+ const urlHash = getUrlHash(url);
37
+ const statePath = getStatePath(accountIdx, urlHash);
38
+ if (fs.existsSync(statePath)) {
39
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
40
+ log.info(`[WindowState] Loaded state for ${accountIdx}-${urlHash} (${url})`);
41
+ return state;
42
+ }
43
+ } catch (error) {
44
+ log.error(`[WindowState] Failed to load state:`, error);
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // 保存窗口状态
50
+ function saveWindowState(accountIdx, url, state) {
51
+ try {
52
+ ensureStateDir();
53
+ const urlHash = getUrlHash(url);
54
+ const statePath = getStatePath(accountIdx, urlHash);
55
+ const stateWithUrl = { ...state, url, domain: new URL(url).hostname };
56
+ fs.writeFileSync(statePath, JSON.stringify(stateWithUrl, null, 2), "utf8");
57
+ log.info(`[WindowState] Saved state for ${accountIdx}-${urlHash}`);
58
+ } catch (error) {
59
+ log.error(`[WindowState] Failed to save state:`, error);
60
+ }
61
+ }
62
+
63
+ // 删除窗口状态
64
+ function removeWindowState(accountIdx, url) {
65
+ try {
66
+ const urlHash = getUrlHash(url);
67
+ const statePath = getStatePath(accountIdx, urlHash);
68
+ if (fs.existsSync(statePath)) {
69
+ fs.unlinkSync(statePath);
70
+ log.info(`[WindowState] Removed state for ${accountIdx}-${urlHash}`);
71
+ }
72
+ } catch (error) {
73
+ log.error(`[WindowState] Failed to remove state:`, error);
74
+ }
75
+ }
76
+
77
+ // 监听窗口状态变化
78
+ function watchWindowState(win, accountIdx) {
79
+ let saveTimeout = null;
80
+ let currentUrl = win.webContents.getURL();
81
+
82
+ const saveState = () => {
83
+ if (win.isDestroyed()) return;
84
+
85
+ const url = win.webContents.getURL();
86
+ if (!url || url === "about:blank") return;
87
+
88
+ const bounds = win.getBounds();
89
+ const state = {
90
+ x: bounds.x,
91
+ y: bounds.y,
92
+ width: bounds.width,
93
+ height: bounds.height,
94
+ isMaximized: win.isMaximized(),
95
+ isFullScreen: win.isFullScreen(),
96
+ updatedAt: new Date().toISOString(),
97
+ };
98
+
99
+ saveWindowState(accountIdx, url, state);
100
+ currentUrl = url;
101
+ };
102
+
103
+ // 防抖保存
104
+ const debouncedSave = () => {
105
+ if (saveTimeout) clearTimeout(saveTimeout);
106
+ saveTimeout = setTimeout(saveState, 500);
107
+ };
108
+
109
+ // 监听窗口事件
110
+ win.on("resize", debouncedSave);
111
+ win.on("move", debouncedSave);
112
+ win.on("maximize", saveState);
113
+ win.on("unmaximize", saveState);
114
+ win.on("enter-full-screen", saveState);
115
+ win.on("leave-full-screen", saveState);
116
+
117
+ // URL变化时保存
118
+ win.webContents.on("did-navigate", () => {
119
+ const newUrl = win.webContents.getURL();
120
+ if (newUrl !== currentUrl) {
121
+ saveState();
122
+ }
123
+ });
124
+
125
+ // 窗口关闭时保存最终状态(不删除)
126
+ win.on("closed", () => {
127
+ if (saveTimeout) clearTimeout(saveTimeout);
128
+ saveState();
129
+ });
130
+ }
131
+
132
+ module.exports = {
133
+ loadWindowState,
134
+ saveWindowState,
135
+ removeWindowState,
136
+ watchWindowState,
137
+ };
@@ -0,0 +1,336 @@
1
+ const { app, BrowserWindow, Menu, dialog, shell } = require("electron");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const log = require("electron-log");
6
+ const { config } = require("../config");
7
+ const { initWindowMonitoring } = require("./window-monitor");
8
+ const { loadWindowState, watchWindowState } = require("./window-state");
9
+
10
+ app.name = "ElectronMCP";
11
+
12
+ function setupWindowHandlers(win) {
13
+
14
+ // Hook window.open to use createWindow with proper webPreferences (webviewTag etc)
15
+ win.webContents.setWindowOpenHandler(({ url }) => {
16
+ log.info(`[WindowOpen] Intercepted: ${url}`);
17
+ if (url && url !== "about:blank") {
18
+ showOpenLinkDialog(win, url);
19
+ }
20
+ return { action: "deny" };
21
+ });
22
+ if (!win.webContents.debugger.isAttached()) {
23
+ win.webContents.debugger.attach("1.3");
24
+ }
25
+
26
+ // 初始化窗口监控(在 dom-ready 之前调用)
27
+ initWindowMonitoring(win);
28
+
29
+ // 🔥 确保窗口可以正常关闭 + 添加日志
30
+ win.on("close", () => {
31
+ log.info(`[Window ${win.id}] Close event triggered: ${win.getTitle()}`);
32
+ });
33
+
34
+ // 🔥 全局下载处理 - 自动保存到 ~/Downloads/electron/
35
+ const ses = win.webContents.session;
36
+ if (!ses._autoDownloadEnabled) {
37
+ ses._autoDownloadEnabled = true;
38
+ ses.on("will-download", (event, item, webContents) => {
39
+ // 如果没有设置 savePath,自动保存
40
+ setTimeout(() => {
41
+ if (!item.getSavePath()) {
42
+ const filename = item.getFilename();
43
+ const savePath = path.join(app.getPath("home"), "Downloads", "electron", filename);
44
+ fs.mkdirSync(path.dirname(savePath), { recursive: true });
45
+ item.setSavePath(savePath);
46
+ log.info(`[Auto Download] ${filename} -> ${savePath}`);
47
+ }
48
+ }, 0);
49
+ });
50
+ }
51
+
52
+ win.webContents.on("dom-ready", async () => {
53
+ // Auto-inject electronRPC for trusted URLs
54
+ const pageUrl = win.webContents.getURL();
55
+ try {
56
+ const u = new URL(pageUrl);
57
+ if (u.hostname === "localhost" || u.hostname.endsWith(".de5.net")) {
58
+ const rpcCode = `
59
+ if (!window.electronRPC) {
60
+ try {
61
+ const { ipcRenderer } = require('electron');
62
+ window.electronRPC = (tool, args) => ipcRenderer.invoke('rpc', tool, args || {});
63
+ console.log('[RPC] electronRPC ready');
64
+ } catch(e) {}
65
+ }
66
+ `;
67
+ if (win.webContents.debugger.isAttached()) {
68
+ await win.webContents.debugger.sendCommand("Runtime.evaluate", { expression: rpcCode });
69
+ } else {
70
+ await win.webContents.executeJavaScript(rpcCode);
71
+ }
72
+ }
73
+ } catch(e) { log.error("[RPC inject]", e.message); }
74
+
75
+ try {
76
+ // 1. 获取当前页面的根域名
77
+ const currentURL = win.webContents.getURL();
78
+ const url = new URL(currentURL);
79
+ const hostname = url.hostname;
80
+ const port = url.port;
81
+
82
+ // 2. 确定域名标识
83
+ let domain;
84
+ if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
85
+ // localhost 或 IP 地址,使用 hostname:port 作为标识
86
+ domain = port ? `${hostname}_${port}` : hostname;
87
+ } else {
88
+ // 提取根域名 (例如: web.telegram.org -> telegram.org)
89
+ const parts = hostname.split(".");
90
+ domain = parts.length > 2 ? parts.slice(-2).join(".") : hostname;
91
+ }
92
+
93
+ // 3. 检查域名注入脚本
94
+ const injectDir = path.join(os.homedir(), "data", "electron", "extension", "inject");
95
+ const injectFile = path.join(injectDir, `${domain}.js`);
96
+
97
+ // 4. 确保目录存在
98
+ if (!fs.existsSync(injectDir)) {
99
+ fs.mkdirSync(injectDir, { recursive: true });
100
+ }
101
+
102
+ let domainCode = "";
103
+
104
+ // 5. 如果文件不存在,使用默认脚本并创建文件
105
+ if (!fs.existsSync(injectFile)) {
106
+ const defaultInjectPath = path.join(__dirname, "..", "extension", "inject.js");
107
+ domainCode = fs.readFileSync(defaultInjectPath, "utf-8");
108
+ fs.writeFileSync(injectFile, domainCode, "utf-8");
109
+ log.info(`[DomReady] Created inject script for ${domain}`);
110
+ } else {
111
+ domainCode = fs.readFileSync(injectFile, "utf-8");
112
+ }
113
+
114
+ // 6. 注入脚本
115
+ await win.webContents.executeJavaScript(`
116
+ (async () => {
117
+ try {
118
+ ${domainCode}
119
+ } catch(e) {
120
+ log.error('Domain inject error:', e);
121
+ }
122
+ })()
123
+ `);
124
+ log.info(`[DomReady] Injected script for ${domain}`);
125
+ } catch (error) {
126
+ log.error("[DomReady] Error:", error);
127
+ }
128
+ });
129
+ }
130
+
131
+ function isTrustedUrl(url) {
132
+ if (!url) return false;
133
+ try {
134
+ const u = new URL(url);
135
+ return u.hostname === "localhost" || u.hostname.endsWith(".de5.net");
136
+ } catch { return false; }
137
+ }
138
+
139
+ function createWindow(options = {}, accountIdx = 0, forceNew = false) {
140
+ const { width = 1200, height = 800, url, webPreferences = {}, x, y } = options;
141
+ console.log("[createWindow] url:", url, "isTrusted:", isTrustedUrl(url));
142
+
143
+ // Check if oneWindow mode is enabled - execute before coordinate logic
144
+ if (config.oneWindow && !forceNew) {
145
+ const allWindows = BrowserWindow.getAllWindows();
146
+ if (allWindows.length > 0) {
147
+ const existingWin = allWindows[0];
148
+ log.info(
149
+ `[WindowUtils] Single window mode enabled. Reusing existing window ${existingWin.id}`
150
+ );
151
+
152
+ if (existingWin.isMinimized()) existingWin.restore();
153
+ existingWin.focus();
154
+
155
+ if (url) {
156
+ const currentUrl = existingWin.webContents.getURL();
157
+ if (currentUrl === url) {
158
+ log.info(`[WindowUtils] Same URL detected, reloading page`);
159
+ existingWin.webContents.reload();
160
+ } else {
161
+ existingWin.loadURL(url);
162
+ }
163
+ }
164
+ return existingWin;
165
+ }
166
+ }
167
+
168
+ // 尝试加载保存的窗口状态(基于URL)
169
+ const savedState = url ? loadWindowState(accountIdx, url) : null;
170
+
171
+ // 如果没有指定位置和大小,使用保存的状态或自动偏移
172
+ let posX = x;
173
+ let posY = y;
174
+ let winWidth = width;
175
+ let winHeight = height;
176
+
177
+ // 只有在没有明确指定位置时才使用保存的状态
178
+ if (x === undefined && y === undefined && savedState) {
179
+ posX = savedState.x;
180
+ posY = savedState.y;
181
+ log.info(`[WindowState] Restored position for ${url}: ${posX},${posY}`);
182
+ } else if (posX === undefined || posY === undefined) {
183
+ const allWindows = BrowserWindow.getAllWindows();
184
+ const offset = allWindows.length * 30; // 每个窗口偏移30px
185
+ posX = posX !== undefined ? posX : offset;
186
+ posY = posY !== undefined ? posY : offset;
187
+ }
188
+
189
+ // 只有在没有明确指定大小时才使用保存的状态
190
+ if (width === 1200 && height === 800 && savedState) {
191
+ winWidth = savedState.width;
192
+ winHeight = savedState.height;
193
+ log.info(`[WindowState] Restored size for ${url}: ${winWidth}x${winHeight}`);
194
+ }
195
+
196
+ const win = new BrowserWindow({
197
+ width: winWidth,
198
+ height: winHeight,
199
+ x: posX,
200
+ y: posY,
201
+ webPreferences: {
202
+ webviewTag: true,
203
+ offscreen: false, // 确保不是离屏渲染
204
+ nodeIntegration: isTrustedUrl(url),
205
+ contextIsolation: !isTrustedUrl(url),
206
+ partition: `persist:sandbox-${accountIdx}`,
207
+ // 启用剪贴板权限
208
+ enableClipboard: true,
209
+ // 允许 webview 访问剪贴板
210
+ webSecurity: false, // 在开发环境中可以考虑禁用,生产环境需要谨慎
211
+ ...webPreferences,
212
+ },
213
+ });
214
+
215
+ // 监听窗口状态变化并自动保存(基于URL)
216
+ watchWindowState(win, accountIdx);
217
+
218
+ // ✅ 核心修正:获取当前窗口真正使用的那个 session
219
+ const ses = win.webContents.session;
220
+
221
+ // 设置代理(如果全局配置了)
222
+ if (config.proxy) {
223
+ const proxyConfig = {
224
+ proxyRules: config.proxy,
225
+ // proxyBypassRules removed
226
+ };
227
+ ses
228
+ .setProxy(proxyConfig)
229
+ .then(() => {
230
+ log.info(`[Proxy] Account ${accountIdx} 已设置代理: ${config.proxy}`);
231
+ })
232
+ .catch((err) => {
233
+ log.error(`[Proxy] Account ${accountIdx} 设置代理失败:`, err);
234
+ });
235
+ }
236
+ ses.setPermissionRequestHandler((webContents, permission, callback) => {
237
+ // 允许麦克风权限(语音输入需要)
238
+ if (permission === "media") {
239
+ log.info(`[Permission] 已自动允许: ${permission}`);
240
+ return callback(true);
241
+ }
242
+ // 允许剪贴板权限
243
+ if (permission.startsWith("clipboard")) {
244
+ log.info(`[Permission] 已自动允许剪贴板权限: ${permission}`);
245
+ return callback(true);
246
+ }
247
+ log.info(`[Permission] 已自动拒绝: ${permission}`);
248
+ return callback(false);
249
+ });
250
+
251
+ // 💡 额外保险:处理权限检查(某些新版 Electron 需要这个)
252
+ ses.setPermissionCheckHandler((webContents, permission, originatingOrigin) => {
253
+ if (permission === "media") return true;
254
+ // 允许剪贴板权限检查
255
+ if (permission.startsWith("clipboard")) return true;
256
+ return false;
257
+ });
258
+
259
+ function getTitlePrefix() {
260
+ return `${config.port}:${accountIdx}-${win.id} | `;
261
+ }
262
+
263
+ const titlePrefix = getTitlePrefix();
264
+
265
+ win.webContents.on("page-title-updated", (event, title) => {
266
+ win.setTitle(`${getTitlePrefix()} | ${title}`);
267
+ });
268
+
269
+ setupWindowHandlers(win);
270
+
271
+ if (url) {
272
+ win.loadURL(url);
273
+ }
274
+
275
+ return win;
276
+ }
277
+
278
+ function getWindowInfo(win) {
279
+ try {
280
+ const wc = win.webContents;
281
+ if (!wc || !wc.session) return null;
282
+ const partition = wc.session.partition || "";
283
+ const accountIdx = partition.startsWith("persist:sandbox-")
284
+ ? parseInt(partition.replace("persist:sandbox-", ""), 10)
285
+ : 0;
286
+
287
+ return {
288
+ id: win.id,
289
+ title: win.getTitle(),
290
+ url: wc.getURL(),
291
+ accountIdx,
292
+ partition,
293
+ debuggerIsAttached: wc.debugger.isAttached(),
294
+ isActive: win.isFocused(),
295
+ bounds: win.getBounds(),
296
+ isDomReady: !wc.isLoading(),
297
+ isLoading: wc.isLoading(),
298
+ isDestroyed: wc.isDestroyed(),
299
+ isCrashed: wc.isCrashed(),
300
+ isWaitingForResponse: wc.isWaitingForResponse(),
301
+ isVisible: win.isVisible(),
302
+ isMinimized: win.isMinimized(),
303
+ isMaximized: win.isMaximized(),
304
+ };
305
+ } catch (e) {
306
+ return null;
307
+ }
308
+ }
309
+
310
+ app.on("browser-window-created", (event, win) => {
311
+ setupWindowHandlers(win);
312
+ });
313
+
314
+
315
+ function showOpenLinkDialog(parentWin, url) {
316
+ dialog.showMessageBox(parentWin, {
317
+ type: 'question',
318
+ buttons: ['Open in Browser', 'Open in App', 'Cancel'],
319
+ defaultId: 0,
320
+ title: 'Open Link',
321
+ message: 'How would you like to open this link?',
322
+ detail: url
323
+ }).then(({ response }) => {
324
+ if (response === 0) {
325
+ shell.openExternal(url);
326
+ } else if (response === 1) {
327
+ createWindow({ url }, 0, true);
328
+ }
329
+ });
330
+ }
331
+
332
+ module.exports = {
333
+ createWindow,
334
+ setupWindowHandlers,
335
+ getWindowInfo,
336
+ };
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+
3
+ echo "🔄 更新桌面启动脚本..."
4
+
5
+ # 设置路径
6
+ DESKTOP_FILE="$HOME/Desktop/electron-mcp.command"
7
+ SOURCE_FILE="./electron-mcp-simple.command"
8
+
9
+ # 备份现有文件
10
+ if [ -f "$DESKTOP_FILE" ]; then
11
+ echo "📋 备份现有文件..."
12
+ cp "$DESKTOP_FILE" "$DESKTOP_FILE.old"
13
+ fi
14
+
15
+ # 复制新文件
16
+ echo "📁 复制新文件..."
17
+ cp "$SOURCE_FILE" "$DESKTOP_FILE"
18
+
19
+ # 设置权限
20
+ echo "🔐 设置执行权限..."
21
+ chmod +x "$DESKTOP_FILE"
22
+
23
+ # 验证
24
+ if [ -f "$DESKTOP_FILE" ] && [ -x "$DESKTOP_FILE" ]; then
25
+ echo "✅ 更新完成!"
26
+ echo "📁 文件: $DESKTOP_FILE"
27
+ echo "🔐 权限: $(ls -l "$DESKTOP_FILE" | cut -d' ' -f1)"
28
+ echo ""
29
+ echo "现在可以双击桌面上的 electron-mcp.command 启动服务了!"
30
+ else
31
+ echo "❌ 更新失败"
32
+ exit 1
33
+ fi