@midscene/shared 1.0.1-beta-20251208031823.0 → 1.0.1-beta-20251208031856.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/dist/es/mcp/base-server.mjs +241 -0
- package/dist/es/mcp/base-tools.mjs +84 -0
- package/dist/es/mcp/index.mjs +4 -0
- package/dist/es/mcp/tool-generator.mjs +215 -0
- package/dist/es/mcp/types.mjs +3 -0
- package/dist/es/node/fs.mjs +1 -1
- package/dist/lib/baseDB.js +2 -2
- package/dist/lib/build/copy-static.js +2 -2
- package/dist/lib/build/rspack-config.js +2 -2
- package/dist/lib/common.js +2 -2
- package/dist/lib/constants/example-code.js +2 -2
- package/dist/lib/constants/index.js +2 -2
- package/dist/lib/env/basic.js +2 -2
- package/dist/lib/env/constants.js +2 -2
- package/dist/lib/env/global-config-manager.js +2 -2
- package/dist/lib/env/helper.js +2 -2
- package/dist/lib/env/index.js +6 -6
- package/dist/lib/env/init-debug.js +2 -2
- package/dist/lib/env/model-config-manager.js +2 -2
- package/dist/lib/env/parse-model-config.js +2 -2
- package/dist/lib/env/types.js +2 -2
- package/dist/lib/env/utils.js +2 -2
- package/dist/lib/extractor/constants.js +2 -2
- package/dist/lib/extractor/debug.js +1 -1
- package/dist/lib/extractor/dom-util.js +2 -2
- package/dist/lib/extractor/index.js +2 -2
- package/dist/lib/extractor/locator.js +2 -2
- package/dist/lib/extractor/tree.js +2 -2
- package/dist/lib/extractor/util.js +2 -2
- package/dist/lib/extractor/web-extractor.js +2 -2
- package/dist/lib/img/box-select.js +2 -2
- package/dist/lib/img/draw-box.js +2 -2
- package/dist/lib/img/get-jimp.js +2 -2
- package/dist/lib/img/get-photon.js +2 -2
- package/dist/lib/img/get-sharp.js +2 -2
- package/dist/lib/img/index.js +2 -2
- package/dist/lib/img/info.js +2 -2
- package/dist/lib/img/transform.js +2 -2
- package/dist/lib/index.js +2 -2
- package/dist/lib/logger.js +2 -2
- package/dist/lib/mcp/base-server.js +281 -0
- package/dist/lib/mcp/base-tools.js +118 -0
- package/dist/lib/mcp/index.js +79 -0
- package/dist/lib/mcp/tool-generator.js +252 -0
- package/dist/lib/mcp/types.js +40 -0
- package/dist/lib/node/fs.js +3 -3
- package/dist/lib/node/index.js +2 -2
- package/dist/lib/polyfills/async-hooks.js +2 -2
- package/dist/lib/polyfills/index.js +2 -2
- package/dist/lib/types/index.js +2 -2
- package/dist/lib/us-keyboard-layout.js +2 -2
- package/dist/lib/utils.js +2 -2
- package/dist/types/mcp/base-server.d.ts +77 -0
- package/dist/types/mcp/base-tools.d.ts +51 -0
- package/dist/types/mcp/index.d.ts +4 -0
- package/dist/types/mcp/tool-generator.d.ts +11 -0
- package/dist/types/mcp/types.d.ts +98 -0
- package/package.json +17 -3
- package/src/mcp/base-server.ts +419 -0
- package/src/mcp/base-tools.ts +190 -0
- package/src/mcp/index.ts +4 -0
- package/src/mcp/tool-generator.ts +311 -0
- package/src/mcp/types.ts +106 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { setIsMcp } from "@midscene/shared/utils";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
function _define_property(obj, key, value) {
|
|
7
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
8
|
+
value: value,
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true
|
|
12
|
+
});
|
|
13
|
+
else obj[key] = value;
|
|
14
|
+
return obj;
|
|
15
|
+
}
|
|
16
|
+
const CLI_ARGS_CONFIG = {
|
|
17
|
+
mode: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: 'stdio'
|
|
20
|
+
},
|
|
21
|
+
port: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
default: '3000'
|
|
24
|
+
},
|
|
25
|
+
host: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
default: 'localhost'
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function launchMCPServer(server, args) {
|
|
31
|
+
if ('http' === args.mode) return server.launchHttp({
|
|
32
|
+
port: Number.parseInt(args.port || '3000', 10),
|
|
33
|
+
host: args.host || 'localhost'
|
|
34
|
+
});
|
|
35
|
+
return server.launch();
|
|
36
|
+
}
|
|
37
|
+
const SESSION_TIMEOUT_MS = 1800000;
|
|
38
|
+
const CLEANUP_INTERVAL_MS = 300000;
|
|
39
|
+
const MAX_SESSIONS = 100;
|
|
40
|
+
class BaseMCPServer {
|
|
41
|
+
async initializeToolsManager() {
|
|
42
|
+
setIsMcp(true);
|
|
43
|
+
this.toolsManager = this.createToolsManager();
|
|
44
|
+
try {
|
|
45
|
+
await this.toolsManager.initTools();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
console.error(`Failed to initialize tools: ${message}`);
|
|
49
|
+
console.error('Tools will be initialized on first use');
|
|
50
|
+
}
|
|
51
|
+
this.toolsManager.attachToServer(this.mcpServer);
|
|
52
|
+
}
|
|
53
|
+
performCleanup() {
|
|
54
|
+
console.error(`${this.config.name} closing...`);
|
|
55
|
+
this.mcpServer.close();
|
|
56
|
+
this.toolsManager?.closeBrowser?.().catch(console.error);
|
|
57
|
+
}
|
|
58
|
+
async launch() {
|
|
59
|
+
await this.initializeToolsManager();
|
|
60
|
+
const transport = new StdioServerTransport();
|
|
61
|
+
try {
|
|
62
|
+
await this.mcpServer.connect(transport);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
console.error(`Failed to connect MCP stdio transport: ${message}`);
|
|
66
|
+
throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
|
|
67
|
+
}
|
|
68
|
+
process.on('uncaughtException', (error)=>{
|
|
69
|
+
console.error(`[${this.config.name}] Uncaught Exception:`, error);
|
|
70
|
+
console.error('Stack:', error.stack);
|
|
71
|
+
});
|
|
72
|
+
process.on('unhandledRejection', (reason)=>{
|
|
73
|
+
console.error(`[${this.config.name}] Unhandled Rejection:`, reason);
|
|
74
|
+
if (reason instanceof Error) console.error('Stack:', reason.stack);
|
|
75
|
+
});
|
|
76
|
+
process.stdin.on('close', ()=>this.performCleanup());
|
|
77
|
+
const cleanup = ()=>{
|
|
78
|
+
console.error(`${this.config.name} shutting down...`);
|
|
79
|
+
this.performCleanup();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
};
|
|
82
|
+
process.once('SIGINT', cleanup);
|
|
83
|
+
process.once('SIGTERM', cleanup);
|
|
84
|
+
}
|
|
85
|
+
async launchHttp(options) {
|
|
86
|
+
if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`);
|
|
87
|
+
await this.initializeToolsManager();
|
|
88
|
+
const express = await import("express");
|
|
89
|
+
const app = express.default();
|
|
90
|
+
app.use(express.default.json({
|
|
91
|
+
limit: '10mb'
|
|
92
|
+
}));
|
|
93
|
+
const sessions = new Map();
|
|
94
|
+
app.all('/mcp', async (req, res)=>{
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
const requestId = randomUUID().substring(0, 8);
|
|
97
|
+
try {
|
|
98
|
+
const rawSessionId = req.headers['mcp-session-id'];
|
|
99
|
+
const sessionId = Array.isArray(rawSessionId) ? rawSessionId[0] : rawSessionId;
|
|
100
|
+
let session = sessionId ? sessions.get(sessionId) : void 0;
|
|
101
|
+
if (!session && 'POST' === req.method) {
|
|
102
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
103
|
+
console.error(`[${new Date().toISOString()}] [${requestId}] Session limit reached: ${sessions.size}/${MAX_SESSIONS}`);
|
|
104
|
+
res.status(503).json({
|
|
105
|
+
error: 'Too many active sessions',
|
|
106
|
+
message: 'Server is at maximum capacity. Please try again later.'
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
session = await this.createHttpSession(sessions);
|
|
111
|
+
console.log(`[${new Date().toISOString()}] [${requestId}] New session created: ${session.transport.sessionId}`);
|
|
112
|
+
}
|
|
113
|
+
if (session) {
|
|
114
|
+
session.lastAccessedAt = new Date();
|
|
115
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
116
|
+
const duration = Date.now() - startTime;
|
|
117
|
+
console.log(`[${new Date().toISOString()}] [${requestId}] Request completed in ${duration}ms`);
|
|
118
|
+
} else {
|
|
119
|
+
console.error(`[${new Date().toISOString()}] [${requestId}] Invalid session or GET without session`);
|
|
120
|
+
res.status(400).json({
|
|
121
|
+
error: 'Invalid session or GET without session'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
+
const duration = Date.now() - startTime;
|
|
127
|
+
console.error(`[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`);
|
|
128
|
+
if (!res.headersSent) res.status(500).json({
|
|
129
|
+
error: 'Internal server error',
|
|
130
|
+
message: 'Failed to process MCP request'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const host = options.host || 'localhost';
|
|
135
|
+
const server = app.listen(options.port, host, ()=>{
|
|
136
|
+
console.log(`${this.config.name} HTTP server listening on http://${host}:${options.port}/mcp`);
|
|
137
|
+
}).on('error', (error)=>{
|
|
138
|
+
if ('EADDRINUSE' === error.code) console.error(`ERROR: Port ${options.port} is already in use.\nPlease try a different port: --port=<number>\nExample: --mode=http --port=${options.port + 1}`);
|
|
139
|
+
else if ('EACCES' === error.code) console.error(`ERROR: Permission denied to bind to port ${options.port}.\nPorts below 1024 require root/admin privileges.\nPlease use a port above 1024 or run with elevated privileges.`);
|
|
140
|
+
else console.error(`ERROR: Failed to start HTTP server on ${host}:${options.port}\nReason: ${error.message}\nCode: ${error.code || 'unknown'}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
143
|
+
const cleanupInterval = this.startSessionCleanup(sessions);
|
|
144
|
+
this.setupHttpShutdownHandlers(server, sessions, cleanupInterval);
|
|
145
|
+
}
|
|
146
|
+
async createHttpSession(sessions) {
|
|
147
|
+
const transport = new StreamableHTTPServerTransport({
|
|
148
|
+
sessionIdGenerator: ()=>randomUUID(),
|
|
149
|
+
onsessioninitialized: (sid)=>{
|
|
150
|
+
sessions.set(sid, {
|
|
151
|
+
transport,
|
|
152
|
+
createdAt: new Date(),
|
|
153
|
+
lastAccessedAt: new Date()
|
|
154
|
+
});
|
|
155
|
+
console.log(`[${new Date().toISOString()}] Session ${sid} initialized (total: ${sessions.size})`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
transport.onclose = ()=>{
|
|
159
|
+
if (transport.sessionId) {
|
|
160
|
+
sessions.delete(transport.sessionId);
|
|
161
|
+
console.log(`[${new Date().toISOString()}] Session ${transport.sessionId} closed (remaining: ${sessions.size})`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
await this.mcpServer.connect(transport);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
console.error(`[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`);
|
|
169
|
+
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
170
|
+
throw new Error(`Failed to initialize MCP session: ${message}`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
transport,
|
|
174
|
+
createdAt: new Date(),
|
|
175
|
+
lastAccessedAt: new Date()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
startSessionCleanup(sessions) {
|
|
179
|
+
return setInterval(()=>{
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
for (const [sid, session] of sessions)if (now - session.lastAccessedAt.getTime() > SESSION_TIMEOUT_MS) try {
|
|
182
|
+
session.transport.close();
|
|
183
|
+
sessions.delete(sid);
|
|
184
|
+
console.log(`[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
console.error(`[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`);
|
|
188
|
+
sessions.delete(sid);
|
|
189
|
+
}
|
|
190
|
+
}, CLEANUP_INTERVAL_MS);
|
|
191
|
+
}
|
|
192
|
+
setupHttpShutdownHandlers(server, sessions, cleanupInterval) {
|
|
193
|
+
const cleanup = ()=>{
|
|
194
|
+
console.error(`${this.config.name} shutting down...`);
|
|
195
|
+
clearInterval(cleanupInterval);
|
|
196
|
+
for (const session of sessions.values())try {
|
|
197
|
+
session.transport.close();
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
200
|
+
console.error(`Error closing session during shutdown: ${message}`);
|
|
201
|
+
}
|
|
202
|
+
sessions.clear();
|
|
203
|
+
try {
|
|
204
|
+
server.close(()=>{
|
|
205
|
+
this.performCleanup();
|
|
206
|
+
process.exit(0);
|
|
207
|
+
});
|
|
208
|
+
setTimeout(()=>{
|
|
209
|
+
console.error('Forcefully shutting down after timeout');
|
|
210
|
+
this.performCleanup();
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}, 5000);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
console.error(`Error closing HTTP server: ${message}`);
|
|
216
|
+
this.performCleanup();
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
process.once('SIGINT', cleanup);
|
|
221
|
+
process.once('SIGTERM', cleanup);
|
|
222
|
+
}
|
|
223
|
+
getServer() {
|
|
224
|
+
return this.mcpServer;
|
|
225
|
+
}
|
|
226
|
+
getToolsManager() {
|
|
227
|
+
return this.toolsManager;
|
|
228
|
+
}
|
|
229
|
+
constructor(config){
|
|
230
|
+
_define_property(this, "mcpServer", void 0);
|
|
231
|
+
_define_property(this, "toolsManager", void 0);
|
|
232
|
+
_define_property(this, "config", void 0);
|
|
233
|
+
this.config = config;
|
|
234
|
+
this.mcpServer = new McpServer({
|
|
235
|
+
name: config.name,
|
|
236
|
+
version: config.version,
|
|
237
|
+
description: config.description
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export { BaseMCPServer, CLI_ARGS_CONFIG, launchMCPServer };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parseBase64 } from "@midscene/shared/img";
|
|
2
|
+
import { getDebug } from "@midscene/shared/logger";
|
|
3
|
+
import { generateCommonTools, generateToolsFromActionSpace } from "./tool-generator.mjs";
|
|
4
|
+
function _define_property(obj, key, value) {
|
|
5
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
6
|
+
value: value,
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true
|
|
10
|
+
});
|
|
11
|
+
else obj[key] = value;
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
const debug = getDebug('mcp:base-tools');
|
|
15
|
+
class BaseMidsceneTools {
|
|
16
|
+
preparePlatformTools() {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
async initTools() {
|
|
20
|
+
this.toolDefinitions = [];
|
|
21
|
+
const platformTools = this.preparePlatformTools();
|
|
22
|
+
this.toolDefinitions.push(...platformTools);
|
|
23
|
+
let actionSpace;
|
|
24
|
+
try {
|
|
25
|
+
const agent = await this.ensureAgent();
|
|
26
|
+
actionSpace = await agent.getActionSpace();
|
|
27
|
+
debug('Action space from connected agent:', actionSpace.map((a)=>a.name).join(', '));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
30
|
+
errorMessage.includes('requires a URL') || errorMessage.includes('web_connect') ? debug('Bridge mode detected - agent will be initialized on first web_connect call') : debug('Agent not available yet, using temporary device for action space');
|
|
31
|
+
const tempDevice = this.createTemporaryDevice();
|
|
32
|
+
actionSpace = tempDevice.actionSpace();
|
|
33
|
+
debug('Action space from temporary device:', actionSpace.map((a)=>a.name).join(', '));
|
|
34
|
+
await tempDevice.destroy?.();
|
|
35
|
+
}
|
|
36
|
+
const actionTools = generateToolsFromActionSpace(actionSpace, ()=>this.ensureAgent());
|
|
37
|
+
const commonTools = generateCommonTools(()=>this.ensureAgent());
|
|
38
|
+
this.toolDefinitions.push(...actionTools, ...commonTools);
|
|
39
|
+
debug('Total tools prepared:', this.toolDefinitions.length);
|
|
40
|
+
}
|
|
41
|
+
attachToServer(server) {
|
|
42
|
+
this.mcpServer = server;
|
|
43
|
+
if (0 === this.toolDefinitions.length) debug('Warning: No tools to register. Tools may be initialized lazily.');
|
|
44
|
+
for (const toolDef of this.toolDefinitions)if (toolDef.autoDestroy) this.toolWithAutoDestroy(toolDef.name, toolDef.description, toolDef.schema, toolDef.handler);
|
|
45
|
+
else this.mcpServer.tool(toolDef.name, toolDef.description, toolDef.schema, toolDef.handler);
|
|
46
|
+
debug('Registered', this.toolDefinitions.length, 'tools');
|
|
47
|
+
}
|
|
48
|
+
toolWithAutoDestroy(name, description, schema, handler) {
|
|
49
|
+
if (!this.mcpServer) throw new Error('MCP server not attached');
|
|
50
|
+
this.mcpServer.tool(name, description, schema, async (...args)=>{
|
|
51
|
+
try {
|
|
52
|
+
return await handler(...args);
|
|
53
|
+
} finally{
|
|
54
|
+
if (!process.env.MIDSCENE_MCP_DISABLE_AGENT_AUTO_DESTROY) {
|
|
55
|
+
try {
|
|
56
|
+
await this.agent?.destroy?.();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
debug('Failed to destroy agent during cleanup:', error);
|
|
59
|
+
}
|
|
60
|
+
this.agent = void 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async closeBrowser() {
|
|
66
|
+
await this.agent?.destroy?.();
|
|
67
|
+
}
|
|
68
|
+
buildScreenshotContent(screenshot) {
|
|
69
|
+
const { mimeType, body } = parseBase64(screenshot);
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
type: 'image',
|
|
73
|
+
data: body,
|
|
74
|
+
mimeType
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
constructor(){
|
|
79
|
+
_define_property(this, "mcpServer", void 0);
|
|
80
|
+
_define_property(this, "agent", void 0);
|
|
81
|
+
_define_property(this, "toolDefinitions", []);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export { BaseMidsceneTools };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { parseBase64 } from "@midscene/shared/img";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function getErrorMessage(error) {
|
|
4
|
+
return error instanceof Error ? error.message : String(error);
|
|
5
|
+
}
|
|
6
|
+
function isZodOptional(value) {
|
|
7
|
+
return '_def' in value && value._def?.typeName === 'ZodOptional';
|
|
8
|
+
}
|
|
9
|
+
function isZodObject(value) {
|
|
10
|
+
return '_def' in value && value._def?.typeName === 'ZodObject' && 'shape' in value;
|
|
11
|
+
}
|
|
12
|
+
function unwrapOptional(value) {
|
|
13
|
+
if (isZodOptional(value)) return {
|
|
14
|
+
innerValue: value._def.innerType,
|
|
15
|
+
isOptional: true
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
innerValue: value,
|
|
19
|
+
isOptional: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function isLocateField(value) {
|
|
23
|
+
if (!isZodObject(value)) return false;
|
|
24
|
+
return 'prompt' in value.shape;
|
|
25
|
+
}
|
|
26
|
+
function makePromptOptional(value, wrapInOptional) {
|
|
27
|
+
const newShape = {
|
|
28
|
+
...value.shape
|
|
29
|
+
};
|
|
30
|
+
newShape.prompt = value.shape.prompt.optional();
|
|
31
|
+
let newSchema = z.object(newShape).passthrough();
|
|
32
|
+
if (wrapInOptional) newSchema = newSchema.optional();
|
|
33
|
+
return newSchema;
|
|
34
|
+
}
|
|
35
|
+
function transformSchemaField(key, value) {
|
|
36
|
+
const { innerValue, isOptional } = unwrapOptional(value);
|
|
37
|
+
if (isZodObject(innerValue) && isLocateField(innerValue)) return [
|
|
38
|
+
key,
|
|
39
|
+
makePromptOptional(innerValue, isOptional)
|
|
40
|
+
];
|
|
41
|
+
return [
|
|
42
|
+
key,
|
|
43
|
+
value
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function extractActionSchema(paramSchema) {
|
|
47
|
+
if (!paramSchema) return {};
|
|
48
|
+
const schema = paramSchema;
|
|
49
|
+
if (!isZodObject(schema)) return schema;
|
|
50
|
+
return Object.fromEntries(Object.entries(schema.shape).map(([key, value])=>transformSchemaField(key, value)));
|
|
51
|
+
}
|
|
52
|
+
function serializeArgsToDescription(args) {
|
|
53
|
+
try {
|
|
54
|
+
return Object.entries(args).map(([key, value])=>{
|
|
55
|
+
if ('object' == typeof value && null !== value) try {
|
|
56
|
+
return `${key}: ${JSON.stringify(value)}`;
|
|
57
|
+
} catch {
|
|
58
|
+
return `${key}: [object]`;
|
|
59
|
+
}
|
|
60
|
+
return `${key}: "${value}"`;
|
|
61
|
+
}).join(', ');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const errorMessage = getErrorMessage(error);
|
|
64
|
+
console.error('Error serializing args:', errorMessage);
|
|
65
|
+
return `[args serialization failed: ${errorMessage}]`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function buildActionInstruction(actionName, args) {
|
|
69
|
+
const argsDescription = serializeArgsToDescription(args);
|
|
70
|
+
return argsDescription ? `Use the action "${actionName}" with ${argsDescription}` : `Use the action "${actionName}"`;
|
|
71
|
+
}
|
|
72
|
+
async function captureScreenshotResult(agent, actionName) {
|
|
73
|
+
try {
|
|
74
|
+
const screenshot = await agent.page?.screenshotBase64();
|
|
75
|
+
if (!screenshot) return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: `Action "${actionName}" completed.`
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
const { mimeType, body } = parseBase64(screenshot);
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: `Action "${actionName}" completed.`
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'image',
|
|
92
|
+
data: body,
|
|
93
|
+
mimeType
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const errorMessage = getErrorMessage(error);
|
|
99
|
+
console.error('Error capturing screenshot:', errorMessage);
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: `Action "${actionName}" completed (screenshot unavailable: ${errorMessage})`
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function createErrorResult(message) {
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: message
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
isError: true
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function generateToolsFromActionSpace(actionSpace, getAgent) {
|
|
122
|
+
return actionSpace.map((action)=>{
|
|
123
|
+
const schema = extractActionSchema(action.paramSchema);
|
|
124
|
+
return {
|
|
125
|
+
name: action.name,
|
|
126
|
+
description: action.description || `Execute ${action.name} action`,
|
|
127
|
+
schema,
|
|
128
|
+
handler: async (args)=>{
|
|
129
|
+
try {
|
|
130
|
+
const agent = await getAgent();
|
|
131
|
+
if (agent.aiAction) {
|
|
132
|
+
const instruction = buildActionInstruction(action.name, args);
|
|
133
|
+
try {
|
|
134
|
+
await agent.aiAction(instruction);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const errorMessage = getErrorMessage(error);
|
|
137
|
+
console.error(`Error executing action "${action.name}":`, errorMessage);
|
|
138
|
+
return createErrorResult(`Failed to execute action "${action.name}": ${errorMessage}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return await captureScreenshotResult(agent, action.name);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const errorMessage = getErrorMessage(error);
|
|
144
|
+
console.error(`Error in handler for "${action.name}":`, errorMessage);
|
|
145
|
+
return createErrorResult(`Failed to get agent or execute action "${action.name}": ${errorMessage}`);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
autoDestroy: true
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function generateCommonTools(getAgent) {
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
name: 'take_screenshot',
|
|
156
|
+
description: 'Capture screenshot of current page/screen',
|
|
157
|
+
schema: {},
|
|
158
|
+
handler: async ()=>{
|
|
159
|
+
try {
|
|
160
|
+
const agent = await getAgent();
|
|
161
|
+
const screenshot = await agent.page?.screenshotBase64();
|
|
162
|
+
if (!screenshot) return createErrorResult('Screenshot not available');
|
|
163
|
+
const { mimeType, body } = parseBase64(screenshot);
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: 'image',
|
|
168
|
+
data: body,
|
|
169
|
+
mimeType
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const errorMessage = getErrorMessage(error);
|
|
175
|
+
console.error('Error taking screenshot:', errorMessage);
|
|
176
|
+
return createErrorResult(`Failed to capture screenshot: ${errorMessage}`);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
autoDestroy: true
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'wait_for',
|
|
183
|
+
description: 'Wait until condition becomes true',
|
|
184
|
+
schema: {
|
|
185
|
+
assertion: z.string().describe('Condition to wait for'),
|
|
186
|
+
timeoutMs: z.number().optional().default(15000),
|
|
187
|
+
checkIntervalMs: z.number().optional().default(3000)
|
|
188
|
+
},
|
|
189
|
+
handler: async (args)=>{
|
|
190
|
+
try {
|
|
191
|
+
const agent = await getAgent();
|
|
192
|
+
const { assertion, timeoutMs, checkIntervalMs } = args;
|
|
193
|
+
if (agent.aiWaitFor) await agent.aiWaitFor(assertion, {
|
|
194
|
+
timeoutMs,
|
|
195
|
+
checkIntervalMs
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: 'text',
|
|
201
|
+
text: `Condition met: "${assertion}"`
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const errorMessage = getErrorMessage(error);
|
|
207
|
+
console.error('Error in wait_for:', errorMessage);
|
|
208
|
+
return createErrorResult(`Wait condition failed: ${errorMessage}`);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
autoDestroy: true
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
export { generateCommonTools, generateToolsFromActionSpace };
|