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,454 @@
|
|
|
1
|
+
const { BrowserWindow, clipboard, nativeImage } = require("electron");
|
|
2
|
+
const { z } = require("zod");
|
|
3
|
+
const { sendCDP } = require("../utils/cdp-utils");
|
|
4
|
+
const { createWindow } = require("../utils/window-utils");
|
|
5
|
+
|
|
6
|
+
const GEMINI_URL = "https://gemini.google.com/app";
|
|
7
|
+
|
|
8
|
+
async function ensureGeminiWindow(win_id, accountIdx = 0) {
|
|
9
|
+
let win = win_id ? BrowserWindow.fromId(win_id) : null;
|
|
10
|
+
|
|
11
|
+
if (!win) {
|
|
12
|
+
if (win_id) {
|
|
13
|
+
throw new Error(`Window ${win_id} not found`);
|
|
14
|
+
}
|
|
15
|
+
win = createWindow({ url: GEMINI_URL }, accountIdx, false);
|
|
16
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
17
|
+
return win;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const url = win.webContents.getURL();
|
|
21
|
+
if (!url.includes("gemini.google.com")) {
|
|
22
|
+
await win.loadURL(GEMINI_URL);
|
|
23
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return win;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function registerTools(registerTool) {
|
|
30
|
+
registerTool(
|
|
31
|
+
"gemini_web_ensure",
|
|
32
|
+
"Ensure gemini window exists and is on gemini.google.com",
|
|
33
|
+
z.object({
|
|
34
|
+
win_id: z.number().optional().describe("窗口 ID,不提供则创建新窗口"),
|
|
35
|
+
accountIdx: z.number().optional().default(0).describe("账户索引"),
|
|
36
|
+
}),
|
|
37
|
+
async ({ win_id, accountIdx }) => {
|
|
38
|
+
try {
|
|
39
|
+
const win = await ensureGeminiWindow(win_id, accountIdx);
|
|
40
|
+
const url = win.webContents.getURL();
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify({ win_id: win.id, url }, null, 2) }],
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ tag: "Gemini" }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
registerTool(
|
|
52
|
+
"is_gemini_logged",
|
|
53
|
+
"Check if gemini is logged in",
|
|
54
|
+
z.object({
|
|
55
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
56
|
+
}),
|
|
57
|
+
async ({ win_id }) => {
|
|
58
|
+
try {
|
|
59
|
+
const win = await ensureGeminiWindow(win_id);
|
|
60
|
+
|
|
61
|
+
const result = await win.webContents.executeJavaScript(`
|
|
62
|
+
(function() {
|
|
63
|
+
var href = location.href;
|
|
64
|
+
var richTextarea = document.querySelector('rich-textarea');
|
|
65
|
+
var isLogged = !!richTextarea;
|
|
66
|
+
return JSON.stringify({ href: href, isLogged: isLogged });
|
|
67
|
+
})()
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
const data = JSON.parse(result);
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{ tag: "Gemini" }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
registerTool(
|
|
80
|
+
"gemini_web_status",
|
|
81
|
+
"Get gemini web page status",
|
|
82
|
+
z.object({
|
|
83
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
84
|
+
}),
|
|
85
|
+
async ({ win_id }) => {
|
|
86
|
+
try {
|
|
87
|
+
const win = await ensureGeminiWindow(win_id);
|
|
88
|
+
|
|
89
|
+
const result = await win.webContents.executeJavaScript(`
|
|
90
|
+
(function() {
|
|
91
|
+
var href = location.href;
|
|
92
|
+
var title = document.title;
|
|
93
|
+
var richTextarea = document.querySelector('rich-textarea');
|
|
94
|
+
var editor = richTextarea?.querySelector('div.ql-editor');
|
|
95
|
+
var hasPrompt = !!richTextarea;
|
|
96
|
+
var promptText = (editor?.textContent || '').slice(0, 100);
|
|
97
|
+
var sendBtn = document.querySelector('button[aria-label="Send"]');
|
|
98
|
+
var stopBtn = document.querySelector('button[aria-label="Stop"]');
|
|
99
|
+
var isGenerating = !!stopBtn;
|
|
100
|
+
var hasLoginBtn = document.querySelector('[data-testid="sign-in-button"]');
|
|
101
|
+
var isLogged = !hasLoginBtn && hasPrompt;
|
|
102
|
+
var hasImage = !!document.querySelector('uploader-file-preview img') || !!document.querySelector('file-preview-container img');
|
|
103
|
+
var isUploading = !!document.querySelector('uploader-file-preview[aria-label*="加载"], uploader-file-preview[aria-label*="loading"], div[role="progressbar"]');
|
|
104
|
+
return JSON.stringify({ href, title, isLogged, hasPrompt, promptText, hasSendBtn: !!sendBtn, isGenerating, hasImage, isUploading });
|
|
105
|
+
})()
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
const data = JSON.parse(result);
|
|
109
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{ tag: "Gemini" }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
registerTool(
|
|
118
|
+
"gemini_web_clear_prompt",
|
|
119
|
+
"Clear the gemini prompt input",
|
|
120
|
+
z.object({
|
|
121
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
122
|
+
}),
|
|
123
|
+
async ({ win_id }) => {
|
|
124
|
+
try {
|
|
125
|
+
const win = await ensureGeminiWindow(win_id);
|
|
126
|
+
|
|
127
|
+
await win.webContents.executeJavaScript(`
|
|
128
|
+
(function() {
|
|
129
|
+
var richTextarea = document.querySelector('rich-textarea');
|
|
130
|
+
var editor = richTextarea?.querySelector('div.ql-editor');
|
|
131
|
+
if (editor) {
|
|
132
|
+
editor.innerHTML = '<p><br></p>';
|
|
133
|
+
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
|
134
|
+
}
|
|
135
|
+
})()
|
|
136
|
+
`);
|
|
137
|
+
|
|
138
|
+
return { content: [{ type: "text", text: "Prompt cleared" }] };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{ tag: "Gemini" }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
registerTool(
|
|
147
|
+
"gemini_web_set_prompt",
|
|
148
|
+
"Set the gemini prompt input value",
|
|
149
|
+
z.object({
|
|
150
|
+
win_id: z.number().optional().default(1).describe("窗口 ID"),
|
|
151
|
+
text: z.string().describe("Text to set in prompt"),
|
|
152
|
+
}),
|
|
153
|
+
async ({ win_id, text }) => {
|
|
154
|
+
try {
|
|
155
|
+
const win = await ensureGeminiWindow(win_id);
|
|
156
|
+
|
|
157
|
+
const hasInput = await win.webContents.executeJavaScript(
|
|
158
|
+
`!!document.querySelector('rich-textarea')`
|
|
159
|
+
);
|
|
160
|
+
if (!hasInput) {
|
|
161
|
+
return { content: [{ type: "text", text: "Input area not found" }], isError: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await win.webContents.executeJavaScript(
|
|
165
|
+
`document.querySelector('rich-textarea div.ql-editor').focus()`
|
|
166
|
+
);
|
|
167
|
+
await sendCDP(win.webContents, "Input.insertText", { text });
|
|
168
|
+
|
|
169
|
+
return { content: [{ type: "text", text: "Prompt set to: " + text }] };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{ tag: "Gemini" }
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
registerTool(
|
|
178
|
+
"gemini_web_click_send",
|
|
179
|
+
"Click send button or press Enter in Gemini",
|
|
180
|
+
z.object({
|
|
181
|
+
win_id: z.number().optional(),
|
|
182
|
+
}),
|
|
183
|
+
async (args) => {
|
|
184
|
+
const win_id = args?.win_id || 1;
|
|
185
|
+
try {
|
|
186
|
+
const win = await ensureGeminiWindow(win_id);
|
|
187
|
+
// Use JS to click send instead of CDP to avoid page reload
|
|
188
|
+
await win.webContents.executeJavaScript(`
|
|
189
|
+
(function() {
|
|
190
|
+
var btn = document.querySelector('button[aria-label="Send"]');
|
|
191
|
+
if (btn) { btn.click(); return 'clicked'; }
|
|
192
|
+
var editor = document.querySelector('rich-textarea div.ql-editor');
|
|
193
|
+
if (editor) { editor.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true})); return 'event'; }
|
|
194
|
+
return 'notfound';
|
|
195
|
+
})()
|
|
196
|
+
`);
|
|
197
|
+
return { content: [{ type: "text", text: "sent" }] };
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{ tag: "Gemini" }
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
registerTool(
|
|
206
|
+
"gemini_web_image_clipboard_prompt",
|
|
207
|
+
"Paste image from clipboard to gemini input",
|
|
208
|
+
z.object({
|
|
209
|
+
win_id: z.number().optional(),
|
|
210
|
+
}),
|
|
211
|
+
async (args) => {
|
|
212
|
+
const win_id = args?.win_id || 1;
|
|
213
|
+
try {
|
|
214
|
+
console.log("DEBUG: gemini_web_image_clipboard_prompt called");
|
|
215
|
+
const win = await ensureGeminiWindow(win_id);
|
|
216
|
+
await win.webContents.executeJavaScript(`
|
|
217
|
+
(function() {
|
|
218
|
+
var richTextarea = document.querySelector('rich-textarea');
|
|
219
|
+
if (richTextarea) {
|
|
220
|
+
var editor = richTextarea.querySelector('div.ql-editor');
|
|
221
|
+
if (editor) editor.click();
|
|
222
|
+
}
|
|
223
|
+
})()
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
// Use Electron sendInputEvent for paste
|
|
227
|
+
win.webContents.sendInputEvent({
|
|
228
|
+
type: "keyDown",
|
|
229
|
+
keyCode: "V",
|
|
230
|
+
modifiers: [process.platform==="darwin"?"meta":"control"],
|
|
231
|
+
});
|
|
232
|
+
win.webContents.sendInputEvent({
|
|
233
|
+
type: "char",
|
|
234
|
+
keyCode: "V",
|
|
235
|
+
modifiers: [process.platform==="darwin"?"meta":"control"],
|
|
236
|
+
});
|
|
237
|
+
win.webContents.sendInputEvent({
|
|
238
|
+
type: "keyUp",
|
|
239
|
+
keyCode: "V",
|
|
240
|
+
modifiers: [process.platform==="darwin"?"meta":"control"],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Wait for upload
|
|
244
|
+
var startTime = Date.now();
|
|
245
|
+
var uploadResult = null;
|
|
246
|
+
|
|
247
|
+
while (Date.now() - startTime < 15000) {
|
|
248
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
249
|
+
|
|
250
|
+
var checkResult = await win.webContents.executeJavaScript(`
|
|
251
|
+
(function() {
|
|
252
|
+
var hasImage = !!document.querySelector('uploader-file-preview img');
|
|
253
|
+
var progressBar = document.querySelector('div[role="progressbar"]');
|
|
254
|
+
if (hasImage) return 'success';
|
|
255
|
+
if (progressBar) return 'uploading';
|
|
256
|
+
return 'waiting';
|
|
257
|
+
})()
|
|
258
|
+
`);
|
|
259
|
+
|
|
260
|
+
if (checkResult === "success") {
|
|
261
|
+
uploadResult = { success: true, status: "uploaded" };
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!uploadResult) {
|
|
267
|
+
uploadResult = { success: false, status: "timeout" };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { content: [{ type: "text", text: JSON.stringify(uploadResult, null, 2) }] };
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{ tag: "Gemini" }
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
registerTool(
|
|
279
|
+
"gemini_web_image_base64_prompt",
|
|
280
|
+
"Ask Gemini with base64 image and prompt",
|
|
281
|
+
z.object({
|
|
282
|
+
win_id: z.number().optional(),
|
|
283
|
+
imageBase64: z.string().describe("Base64 image data"),
|
|
284
|
+
prompt: z.string().describe("Question to ask about the image"),
|
|
285
|
+
waitMs: z.number().optional().default(20000).describe("Wait time for response"),
|
|
286
|
+
}),
|
|
287
|
+
async (args) => {
|
|
288
|
+
const win_id = args?.win_id || 1;
|
|
289
|
+
const { imageBase64, prompt, waitMs } = args;
|
|
290
|
+
try {
|
|
291
|
+
const win = await ensureGeminiWindow(win_id);
|
|
292
|
+
|
|
293
|
+
// Write base64 to clipboard as image
|
|
294
|
+
const imgBuffer = Buffer.from(imageBase64, "base64");
|
|
295
|
+
const { nativeImage } = require("electron");
|
|
296
|
+
const img = nativeImage.createFromBuffer(imgBuffer);
|
|
297
|
+
require("electron").clipboard.writeImage(img);
|
|
298
|
+
|
|
299
|
+
// Click input and paste
|
|
300
|
+
await win.webContents.executeJavaScript(`
|
|
301
|
+
(function() {
|
|
302
|
+
var richTextarea = document.querySelector('rich-textarea');
|
|
303
|
+
if (richTextarea) {
|
|
304
|
+
var editor = richTextarea.querySelector('div.ql-editor');
|
|
305
|
+
if (editor) editor.click();
|
|
306
|
+
}
|
|
307
|
+
})()
|
|
308
|
+
`);
|
|
309
|
+
|
|
310
|
+
// Paste image
|
|
311
|
+
win.webContents.sendInputEvent({ type: "keyDown", keyCode: "V", modifiers: [process.platform==="darwin"?"meta":"control"] });
|
|
312
|
+
win.webContents.sendInputEvent({ type: "char", keyCode: "V", modifiers: [process.platform==="darwin"?"meta":"control"] });
|
|
313
|
+
win.webContents.sendInputEvent({ type: "keyUp", keyCode: "V", modifiers: [process.platform==="darwin"?"meta":"control"] });
|
|
314
|
+
|
|
315
|
+
// Wait for upload
|
|
316
|
+
var startTime = Date.now();
|
|
317
|
+
while (Date.now() - startTime < 15000) {
|
|
318
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
319
|
+
var hasImage = await win.webContents.executeJavaScript(`
|
|
320
|
+
(function() { return !!document.querySelector('uploader-file-preview img'); })()
|
|
321
|
+
`);
|
|
322
|
+
if (hasImage) break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Set prompt text
|
|
326
|
+
await win.webContents.executeJavaScript(
|
|
327
|
+
`document.querySelector('rich-textarea div.ql-editor').focus()`
|
|
328
|
+
);
|
|
329
|
+
await sendCDP(win.webContents, "Input.insertText", { text: prompt });
|
|
330
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
331
|
+
|
|
332
|
+
// Send
|
|
333
|
+
await win.webContents.executeJavaScript(`
|
|
334
|
+
(function() {
|
|
335
|
+
var btn = document.querySelector('button[aria-label="Send"]');
|
|
336
|
+
if (btn) btn.click();
|
|
337
|
+
})()
|
|
338
|
+
`);
|
|
339
|
+
|
|
340
|
+
// Wait for response
|
|
341
|
+
while (Date.now() - startTime < waitMs) {
|
|
342
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
343
|
+
var checkResult = await win.webContents.executeJavaScript(`
|
|
344
|
+
(function() {
|
|
345
|
+
var stopBtn = document.querySelector('button[aria-label="Stop"]');
|
|
346
|
+
if (stopBtn) return 'generating';
|
|
347
|
+
var lastResponse = document.querySelector('model-response message-content .markdown');
|
|
348
|
+
if (lastResponse) {
|
|
349
|
+
var text = lastResponse.textContent?.trim();
|
|
350
|
+
if (text && text.length > 0) return 'ok';
|
|
351
|
+
}
|
|
352
|
+
return 'waiting';
|
|
353
|
+
})()
|
|
354
|
+
`);
|
|
355
|
+
|
|
356
|
+
if (checkResult === "ok") {
|
|
357
|
+
var reply = await win.webContents.executeJavaScript(`
|
|
358
|
+
(function() {
|
|
359
|
+
var lastResponse = document.querySelector('model-response message-content .markdown');
|
|
360
|
+
return lastResponse ? lastResponse.textContent?.trim() : '';
|
|
361
|
+
})()
|
|
362
|
+
`);
|
|
363
|
+
var duration = Date.now() - startTime;
|
|
364
|
+
return {
|
|
365
|
+
content: [
|
|
366
|
+
{ type: "text", text: JSON.stringify({ reply, duration_ms: duration }, null, 2) },
|
|
367
|
+
],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { content: [{ type: "text", text: "timeout" }] };
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return { content: [{ type: "text", text: "Error: " + error.message }], isError: true };
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{ tag: "Gemini" }
|
|
378
|
+
);
|
|
379
|
+
// Gemini Vision - 识图工具 (调用现有 gemini-web RPC tools)
|
|
380
|
+
registerTool(
|
|
381
|
+
"gemini_vision",
|
|
382
|
+
"Use Gemini to analyze image",
|
|
383
|
+
z.object({
|
|
384
|
+
image: z.string().describe("Image URL or base64 data:image/png;base64,..."),
|
|
385
|
+
prompt: z.string().optional().default("Describe this image in detail").describe("Question about the image"),
|
|
386
|
+
win_id: z.number().optional().default(2).describe("Gemini window ID"),
|
|
387
|
+
}),
|
|
388
|
+
async ({ image, prompt, win_id }) => {
|
|
389
|
+
const fs = require('fs');
|
|
390
|
+
const https = require('https');
|
|
391
|
+
const http = require('http');
|
|
392
|
+
const { callTool } = require('../utils/tool-utils');
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
// 1. Save image to temp file
|
|
396
|
+
const tmpPath = `/tmp/vision-${Date.now()}.png`;
|
|
397
|
+
if (image.startsWith('data:')) {
|
|
398
|
+
const base64 = image.split(',')[1];
|
|
399
|
+
fs.writeFileSync(tmpPath, Buffer.from(base64, 'base64'));
|
|
400
|
+
} else if (image.startsWith('http')) {
|
|
401
|
+
await new Promise((resolve, reject) => {
|
|
402
|
+
const client = image.startsWith('https') ? https : http;
|
|
403
|
+
client.get(image, (res) => {
|
|
404
|
+
const stream = fs.createWriteStream(tmpPath);
|
|
405
|
+
res.pipe(stream);
|
|
406
|
+
stream.on('finish', () => { stream.close(); resolve(); });
|
|
407
|
+
}).on('error', reject);
|
|
408
|
+
});
|
|
409
|
+
} else {
|
|
410
|
+
throw new Error('Invalid image format');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 2. Copy to clipboard
|
|
414
|
+
await callTool('file_to_clipboard', { path: tmpPath });
|
|
415
|
+
|
|
416
|
+
// 3. Paste to Gemini
|
|
417
|
+
await callTool('gemini_paste_image', { win_id });
|
|
418
|
+
|
|
419
|
+
// 4. Set prompt
|
|
420
|
+
await callTool('gemini_web_set_prompt', { win_id, text: prompt });
|
|
421
|
+
|
|
422
|
+
// 5. Send
|
|
423
|
+
await callTool('gemini_web_click_send', { win_id });
|
|
424
|
+
|
|
425
|
+
// 6. Wait for reply (poll every 2s, max 30s)
|
|
426
|
+
let reply = null;
|
|
427
|
+
for (let i = 0; i < 15; i++) {
|
|
428
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
429
|
+
const result = await callTool('gemini_web_get_last_reply', { win_id });
|
|
430
|
+
const text = result?.content?.[0]?.text || '';
|
|
431
|
+
if (text.length > 10) {
|
|
432
|
+
reply = text;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Cleanup
|
|
438
|
+
fs.unlinkSync(tmpPath);
|
|
439
|
+
|
|
440
|
+
if (!reply) throw new Error('Timeout waiting for Gemini reply');
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, result: reply }, null, 2) }],
|
|
444
|
+
};
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }], isError: true };
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
{ tag: "Gemini" }
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = registerTools;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 统一导出所有工具模块,支持热重载
|
|
2
|
+
|
|
3
|
+
module.exports = [
|
|
4
|
+
require("./ping"),
|
|
5
|
+
require("./r-reset"),
|
|
6
|
+
require("./hook-chatgpt"),
|
|
7
|
+
require("./window-tools"),
|
|
8
|
+
require("./cdp-tools"),
|
|
9
|
+
require("./exec-js"),
|
|
10
|
+
require("./clipboard-tools"),
|
|
11
|
+
require("./exec-tools"),
|
|
12
|
+
require("./file-tools"),
|
|
13
|
+
require("./system-tools"),
|
|
14
|
+
require("./automation-tools"),
|
|
15
|
+
require("./account-tools"),
|
|
16
|
+
require("./download-tools"),
|
|
17
|
+
require("./ipc-bridge"),
|
|
18
|
+
require("./hook-gemini"),
|
|
19
|
+
];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { ipcMain, BrowserWindow } = require("electron");
|
|
2
|
+
const { createWindow } = require("../utils/window-utils");
|
|
3
|
+
const log = require("electron-log");
|
|
4
|
+
const { z } = require("zod");
|
|
5
|
+
|
|
6
|
+
// Register ipcMain listener once
|
|
7
|
+
if (!global._cicyIpcBridge) {
|
|
8
|
+
global._cicyIpcBridge = true;
|
|
9
|
+
|
|
10
|
+
ipcMain.on("cicy-open-window", (event, data) => {
|
|
11
|
+
const { url, title } = data || {};
|
|
12
|
+
if (!url) return;
|
|
13
|
+
log.info(`[IPC Bridge] open-window: ${url} ${title || ""}`);
|
|
14
|
+
const win = createWindow({ url }, 0, true);
|
|
15
|
+
if (title) win.setTitle(title);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
log.info("[IPC Bridge] ipcMain listener registered");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = (registerTool) => {
|
|
22
|
+
registerTool(
|
|
23
|
+
"ipc_bridge_status",
|
|
24
|
+
"Check IPC bridge status",
|
|
25
|
+
z.object({}),
|
|
26
|
+
async () => ({
|
|
27
|
+
content: [{ type: "text", text: `IPC bridge active: ${!!global._cicyIpcBridge}` }],
|
|
28
|
+
}),
|
|
29
|
+
{ tag: "System" }
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
|
|
3
|
+
module.exports = (registerTool) => {
|
|
4
|
+
registerTool(
|
|
5
|
+
"ping",
|
|
6
|
+
"测试 MCP 服务器连接",
|
|
7
|
+
z.object({}),
|
|
8
|
+
async () => {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const utc8Time = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
|
11
|
+
.toISOString()
|
|
12
|
+
.replace("T", " ")
|
|
13
|
+
.slice(0, 19);
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: `Pong v:2 ${utc8Time}` }],
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
{ tag: "System" }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
registerTool(
|
|
22
|
+
"refresh_token",
|
|
23
|
+
"刷新认证 token",
|
|
24
|
+
z.object({}),
|
|
25
|
+
async () => {
|
|
26
|
+
const crypto = require("crypto");
|
|
27
|
+
const fs = require("fs");
|
|
28
|
+
const os = require("os");
|
|
29
|
+
const path = require("path");
|
|
30
|
+
|
|
31
|
+
const newToken = crypto.randomBytes(32).toString("hex");
|
|
32
|
+
const tokenPath = path.join(os.homedir(), "data/electron/token.txt");
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(tokenPath, newToken);
|
|
35
|
+
|
|
36
|
+
// Update global auth manager
|
|
37
|
+
if (global.authManager) {
|
|
38
|
+
global.authManager.authToken = newToken;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: JSON.stringify(
|
|
46
|
+
{
|
|
47
|
+
success: true,
|
|
48
|
+
token: newToken,
|
|
49
|
+
message: "Token refreshed successfully",
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
{ tag: "System" }
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
|
|
3
|
+
module.exports = (registerTool) => {
|
|
4
|
+
registerTool(
|
|
5
|
+
"r-reset",
|
|
6
|
+
"清除 require 缓存,重新加载 tools 和 utils 模块",
|
|
7
|
+
z.object({}),
|
|
8
|
+
async () => {
|
|
9
|
+
let count = 0;
|
|
10
|
+
Object.keys(require.cache).forEach((key) => {
|
|
11
|
+
const k = key.replace(/\\/g, "/");
|
|
12
|
+
if (k.includes("/tools/") || k.includes("/utils/")) {
|
|
13
|
+
delete require.cache[key];
|
|
14
|
+
count++;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: `Cleared ${count} cached modules. Next request will use fresh code.`,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
{ tag: "System" }
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { BrowserWindow } = require("electron");
|
|
2
|
+
const { z } = require("zod");
|
|
3
|
+
const { captureSnapshot } = require("../snapshot-utils");
|
|
4
|
+
|
|
5
|
+
function registerTools(registerTool) {
|
|
6
|
+
registerTool(
|
|
7
|
+
"webpage_screenshot_to_clipboard",
|
|
8
|
+
"截图并复制到剪贴板",
|
|
9
|
+
z.object({ win_id: z.number().optional().describe("窗口 ID") }),
|
|
10
|
+
async ({ win_id }) => {
|
|
11
|
+
try {
|
|
12
|
+
const actualWinId = win_id || 1;
|
|
13
|
+
const win = BrowserWindow.fromId(actualWinId);
|
|
14
|
+
if (!win) throw new Error(`Window ${actualWinId} not found`);
|
|
15
|
+
|
|
16
|
+
const result = await captureSnapshot(win.webContents, { win_id: actualWinId });
|
|
17
|
+
return result;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
21
|
+
isError: true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = registerTools;
|