rl-simulator-core 1.0.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/.env +3 -0
- package/dist/index.d.mts +565 -0
- package/dist/index.d.ts +565 -0
- package/dist/index.js +512 -0
- package/dist/index.mjs +476 -0
- package/package.json +21 -0
- package/src/actions.js +145 -0
- package/src/ai.js +86 -0
- package/src/config.js +83 -0
- package/src/index.js +155 -0
- package/src/runner.js +138 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// src/index.js
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import path3 from "path";
|
|
4
|
+
|
|
5
|
+
// src/runner.js
|
|
6
|
+
import { chromium } from "playwright";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
|
|
10
|
+
// src/ai.js
|
|
11
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
12
|
+
import { generateText } from "ai";
|
|
13
|
+
|
|
14
|
+
// src/config.js
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
dotenv.config();
|
|
17
|
+
var API_KEY = process.env.ARK_API_KEY;
|
|
18
|
+
var ENDPOINT = process.env.EP;
|
|
19
|
+
var BASE_URL = process.env.BASE_URL;
|
|
20
|
+
if (!API_KEY || !ENDPOINT || !BASE_URL) {
|
|
21
|
+
console.warn("Missing environment variables (ARK_API_KEY, EP, BASE_URL). Agent may fail if not provided later.");
|
|
22
|
+
}
|
|
23
|
+
var SYSTEM_PROMPT = `
|
|
24
|
+
Intro to Think Mode
|
|
25
|
+
To explore the performance of our model with different styles of thinking, we introduce four modes of thinking: budgted, efficient, no think, and unrestricted. The model is default to unrestricted think mode.
|
|
26
|
+
- Budgeted think mode: Limit model's internal reasoning and reflections to stay within the specified token budget.
|
|
27
|
+
- Efficient think mode: Model provides a concise internal thinking process with efficient reasoning and reflections. It doesn't have a strict token budget, but should be less verbose and more direct in your thinking.
|
|
28
|
+
- No think mode: Model responds directly to the question without any internal reasoning process or extra thinking tokens. Still follows the template with the minimum required thinking tokens to justify the answer.
|
|
29
|
+
- Unrestricted think mode: Model engages in an internal thinking process with thorough reasoning and reflections. There's an unlimited budget for thinking tokens and can continue thinking until you fully solve the problem.
|
|
30
|
+
Think Mode Assignment
|
|
31
|
+
As the "think mode" sp is not encapsulated yet, you need to manually sew in different sp for different modes of thinking. Here are the sp's:
|
|
32
|
+
NOTE: You should disable "thinking" first: (I know it's a bit counterintuitive.)
|
|
33
|
+
"thinking": {
|
|
34
|
+
"type": "disabled"
|
|
35
|
+
}
|
|
36
|
+
- Unrestricted think mode
|
|
37
|
+
You should begin by detailing the internal reasoning process, and then present the answer to the user. The reasoning process should be enclosed within <think_never_used_51bce0c785ca2f68081bfa7d91973934> and </think_never_used_51bce0c785ca2f68081bfa7d91973934> tags, as follows:
|
|
38
|
+
<think_never_used_51bce0c785ca2f68081bfa7d91973934> reasoning process here </think_never_used_51bce0c785ca2f68081bfa7d91973934> answer here.
|
|
39
|
+
You have different modes of thinking:
|
|
40
|
+
Unrestricted think mode: Engage in an internal thinking process with thorough reasoning and reflections. You have an unlimited budget for thinking tokens and can continue thinking until you fully solve the problem.
|
|
41
|
+
Efficient think mode: Provide a concise internal thinking process with efficient reasoning and reflections. You don't have a strict token budget but be less verbose and more direct in your thinking.
|
|
42
|
+
No think mode: Respond directly to the question without any internal reasoning process or extra thinking tokens. Still follow the template with the minimum required thinking tokens to justify the answer.
|
|
43
|
+
Budgeted think mode: Limit your internal reasoning and reflections to stay within the specified token budget
|
|
44
|
+
Based on the complexity of the problem, select the appropriate mode for reasoning among the provided options listed below.
|
|
45
|
+
Provided Mode(s):
|
|
46
|
+
Unrestricted think
|
|
47
|
+
Action Space
|
|
48
|
+
"""
|
|
49
|
+
## Function Definition
|
|
50
|
+
- You have access to the following functions:
|
|
51
|
+
{"type": "function", "name": "click", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse left single click action."}
|
|
52
|
+
{"type": "function", "name": "left_double", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse left double click action."}
|
|
53
|
+
{"type": "function", "name": "left_triple", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse left triple click action."}
|
|
54
|
+
{"type": "function", "name": "right_single", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Click coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse right single click action."}
|
|
55
|
+
{"type": "function", "name": "drag", "parameters": {"type": "object", "properties": {"start_point": {"type": "string", "description": "Drag start point. The format is: x y"}, "end_point": {"type": "string", "description": "Drag end point. The format is: x y"}}, "required": ["start_point", "end_point"]}, "description": "Mouse left button drag action."}
|
|
56
|
+
{"type": "function", "name": "scroll", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Scroll start position. If not specified, default to execute on the current mouse position. The format is: x y"}, "direction": {"type": "string", "description": "Scroll direction.", "enum": ["up", "down", "left", "right"]}}, "required": ["direction"]}, "description": "Scroll action."}
|
|
57
|
+
{"type": "function", "name": "move_to", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Target coordinates. The format is: x y"}}, "required": ["point"]}, "description": "Mouse move action."}
|
|
58
|
+
{"type": "function", "name": "mouse_down", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Mouse down position. If not specified, default to execute on the current mouse position. The format is: x y"}, "button": {"type": "string", "description": "Down button. Default to left.", "enum": ["left", "right"]}}, "required": []}, "description": "Mouse down action."}
|
|
59
|
+
{"type": "function", "name": "mouse_up", "parameters": {"type": "object", "properties": {"point": {"type": "string", "description": "Mouse up position. If not specified, default to execute on the current mouse position. The format is: x y"}, "button": {"type": "string", "description": "Up button. Default to left.", "enum": ["left", "right"]}}, "required": []}, "description": "Mouse up action."}
|
|
60
|
+
{"type": "function", "name": "type", "parameters": {"type": "object", "properties": {"content": {"type": "string", "description": "Type content. If you want to submit your input, use \\n at the end of content."}}, "required": ["content"]}, "description": "Type content."}
|
|
61
|
+
{"type": "function", "name": "hotkey", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "Hotkeys you want to press. Split keys with a space and use lowercase."}}, "required": ["key"]}, "description": "Press hotkey."}
|
|
62
|
+
{"type": "function", "name": "press", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "Key you want to press. Only one key can be pressed at one time."}}, "required": ["key"]}, "description": "Press key."}
|
|
63
|
+
{"type": "function", "name": "call_user", "parameters": {"type": "object", "properties": {"content": {"type": "string", "description": "Message or information displayed to the user to request their input, feedback, or guidance."}}, "required": []}, "description": "This function is used to interact with the user by displaying a message and requesting their input, feedback, or guidance."}
|
|
64
|
+
{"type": "function", "name": "wait", "parameters": {"type": "object", "properties": {"time": {"type": "string", "description": "Wait time in seconds."}}, "required": []}, "description": "Wait for a while."}
|
|
65
|
+
{"type": "function", "name": "finished", "parameters": {"type": "object", "properties": {"content": {"type": "string", "description": "Provide the final answer or response to complete the task."}}, "required": []}, "description": "This function is used to indicate the completion of a task by providing the final answer or response."}
|
|
66
|
+
|
|
67
|
+
- To call a function, use the following structure without any suffix:
|
|
68
|
+
|
|
69
|
+
<gui_think> reasoning process </gui_think>
|
|
70
|
+
<seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934><function_never_used_51bce0c785ca2f68081bfa7d91973934=example_function_name><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_1>value_1</parameter_never_used_51bce0c785ca2f68081bfa7d91973934><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_2>
|
|
71
|
+
This is the value for the second parameter
|
|
72
|
+
that can span
|
|
73
|
+
multiple lines
|
|
74
|
+
</parameter_never_used_51bce0c785ca2f68081bfa7d91973934></function_never_used_51bce0c785ca2f68081bfa7d91973934></seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934>
|
|
75
|
+
|
|
76
|
+
## Important Notes
|
|
77
|
+
- Function calls must begin with <function_never_used_51bce0c785ca2f68081bfa7d91973934= and end with </function_never_used_51bce0c785ca2f68081bfa7d91973934>.
|
|
78
|
+
- All required parameters must be explicitly provided.
|
|
79
|
+
|
|
80
|
+
## Additional Notes
|
|
81
|
+
- You can execute multiple actions within a single tool call. For example:
|
|
82
|
+
<seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934><function_never_used_51bce0c785ca2f68081bfa7d91973934=example_function_1><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_1>value_1</parameter_never_used_51bce0c785ca2f68081bfa7d91973934><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_2>
|
|
83
|
+
This is the value for the second parameter
|
|
84
|
+
that can span
|
|
85
|
+
multiple lines
|
|
86
|
+
</parameter_never_used_51bce0c785ca2f68081bfa7d91973934></function_never_used_51bce0c785ca2f68081bfa7d91973934><function_never_used_51bce0c785ca2f68081bfa7d91973934=example_function_2><parameter_never_used_51bce0c785ca2f68081bfa7d91973934=example_parameter_3>value_4</parameter_never_used_51bce0c785ca2f68081bfa7d91973934></function_never_used_51bce0c785ca2f68081bfa7d91973934></seed:tool_call_never_used_51bce0c785ca2f68081bfa7d91973934>
|
|
87
|
+
"""`;
|
|
88
|
+
|
|
89
|
+
// src/ai.js
|
|
90
|
+
import path from "path";
|
|
91
|
+
var logFile = path.join(process.cwd(), "ai_traffic.log");
|
|
92
|
+
var openai = createOpenAI({
|
|
93
|
+
baseURL: BASE_URL,
|
|
94
|
+
apiKey: API_KEY,
|
|
95
|
+
compatibility: "compatible"
|
|
96
|
+
});
|
|
97
|
+
async function queryAI(messages) {
|
|
98
|
+
console.log("Querying AI with history length:", messages.length);
|
|
99
|
+
try {
|
|
100
|
+
console.log("\u23F3 \u53D1\u9001\u8BF7\u6C42\u4E2D...");
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
const { text } = await generateText({
|
|
103
|
+
model: openai.chat(ENDPOINT),
|
|
104
|
+
system: SYSTEM_PROMPT,
|
|
105
|
+
messages,
|
|
106
|
+
temperature: 0.1,
|
|
107
|
+
topP: 0.7
|
|
108
|
+
});
|
|
109
|
+
const endTime = Date.now();
|
|
110
|
+
console.log("\u2705 \u8BF7\u6C42\u6210\u529F! \u8017\u65F6:", (endTime - startTime) / 1e3, "\u79D2");
|
|
111
|
+
console.log("\u{1F4E5} \u54CD\u5E94\u5185\u5BB9:", text.substring(0, 500) + "...");
|
|
112
|
+
return text;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error("\u274C AI \u8BF7\u6C42\u5931\u8D25!", error);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function parseActions(aiContent) {
|
|
119
|
+
const actions = [];
|
|
120
|
+
const functionRegex = /<function[^>]*=([^>]+)>([\s\S]*?)<\/function[^>]*>/g;
|
|
121
|
+
let functionMatch;
|
|
122
|
+
while ((functionMatch = functionRegex.exec(aiContent)) !== null) {
|
|
123
|
+
const functionName = functionMatch[1];
|
|
124
|
+
const functionBody = functionMatch[2];
|
|
125
|
+
const params = {};
|
|
126
|
+
const paramRegex = /<parameter[^>]*=([^>]+)>([\s\S]*?)<\/parameter[^>]*>/g;
|
|
127
|
+
let paramMatch;
|
|
128
|
+
while ((paramMatch = paramRegex.exec(functionBody)) !== null) {
|
|
129
|
+
const paramName = paramMatch[1];
|
|
130
|
+
let paramValue = paramMatch[2].trim();
|
|
131
|
+
const cleanedValue = paramValue.replace(/<[^>]+>/g, "").trim();
|
|
132
|
+
params[paramName] = cleanedValue;
|
|
133
|
+
}
|
|
134
|
+
actions.push({
|
|
135
|
+
name: functionName,
|
|
136
|
+
params
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return actions;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/actions.js
|
|
143
|
+
async function executeAction(page, action) {
|
|
144
|
+
console.log(`Executing action: ${action.name}`, action.params);
|
|
145
|
+
const { name, params } = action;
|
|
146
|
+
const viewport = page.viewportSize();
|
|
147
|
+
const transformPoint = (pointStr) => {
|
|
148
|
+
if (!pointStr) return null;
|
|
149
|
+
const [x, y] = pointStr.split(" ").map(Number);
|
|
150
|
+
if (!viewport) return { x, y };
|
|
151
|
+
const actualX = x / 1e3 * viewport.width;
|
|
152
|
+
const actualY = y / 1e3 * viewport.height;
|
|
153
|
+
return { x: actualX, y: actualY };
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
switch (name) {
|
|
157
|
+
case "click":
|
|
158
|
+
if (params.point) {
|
|
159
|
+
const { x, y } = transformPoint(params.point);
|
|
160
|
+
console.log(`Executing click action: (${x}, ${y}) [Original: ${params.point}]`);
|
|
161
|
+
await page.mouse.click(x, y);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case "left_double":
|
|
165
|
+
if (params.point) {
|
|
166
|
+
const { x, y } = transformPoint(params.point);
|
|
167
|
+
await page.mouse.dblclick(x, y);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case "left_triple":
|
|
171
|
+
if (params.point) {
|
|
172
|
+
const { x, y } = transformPoint(params.point);
|
|
173
|
+
await page.mouse.click(x, y, { clickCount: 3 });
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case "right_single":
|
|
177
|
+
if (params.point) {
|
|
178
|
+
const { x, y } = transformPoint(params.point);
|
|
179
|
+
await page.mouse.click(x, y, { button: "right" });
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case "drag":
|
|
183
|
+
if (params.start_point && params.end_point) {
|
|
184
|
+
const start = transformPoint(params.start_point);
|
|
185
|
+
const end = transformPoint(params.end_point);
|
|
186
|
+
await page.mouse.move(start.x, start.y);
|
|
187
|
+
await page.mouse.down();
|
|
188
|
+
await page.mouse.move(end.x, end.y);
|
|
189
|
+
await page.mouse.up();
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case "move_to":
|
|
193
|
+
if (params.point) {
|
|
194
|
+
const { x, y } = transformPoint(params.point);
|
|
195
|
+
await page.mouse.move(x, y);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case "mouse_down":
|
|
199
|
+
if (params.point) {
|
|
200
|
+
const { x, y } = transformPoint(params.point);
|
|
201
|
+
await page.mouse.move(x, y);
|
|
202
|
+
await page.mouse.down({ button: params.button || "left" });
|
|
203
|
+
} else {
|
|
204
|
+
await page.mouse.down({ button: params.button || "left" });
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
case "mouse_up":
|
|
208
|
+
if (params.point) {
|
|
209
|
+
const { x, y } = transformPoint(params.point);
|
|
210
|
+
await page.mouse.move(x, y);
|
|
211
|
+
await page.mouse.up({ button: params.button || "left" });
|
|
212
|
+
} else {
|
|
213
|
+
await page.mouse.up({ button: params.button || "left" });
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
case "type":
|
|
217
|
+
if (params.content) {
|
|
218
|
+
const content = params.content;
|
|
219
|
+
await page.keyboard.type(content);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
case "hotkey":
|
|
223
|
+
if (params.key) {
|
|
224
|
+
const keys = params.key.split(" ").join("+");
|
|
225
|
+
await page.keyboard.press(keys);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
case "press":
|
|
229
|
+
if (params.key) {
|
|
230
|
+
await page.keyboard.press(params.key);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
case "scroll":
|
|
234
|
+
const direction = params.direction;
|
|
235
|
+
if (direction === "down") {
|
|
236
|
+
await page.evaluate(() => window.scrollBy(0, 500));
|
|
237
|
+
} else if (direction === "up") {
|
|
238
|
+
await page.evaluate(() => window.scrollBy(0, -500));
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case "wait":
|
|
242
|
+
if (params.time) {
|
|
243
|
+
const ms = parseFloat(params.time) * 1e3;
|
|
244
|
+
await page.waitForTimeout(ms);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "finished":
|
|
248
|
+
console.log("AI Task Finished:", params.content);
|
|
249
|
+
return "FINISHED";
|
|
250
|
+
case "call_user":
|
|
251
|
+
console.log("AI requests user input:", params.content);
|
|
252
|
+
return "FINISHED";
|
|
253
|
+
default:
|
|
254
|
+
console.warn(`Unknown action: ${name}`);
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.error(`Failed to execute action ${name}:`, e);
|
|
258
|
+
}
|
|
259
|
+
return "CONTINUE";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/runner.js
|
|
263
|
+
async function runAgent({ targetUrl, taskInput, sessionId, simulatedUserKnownInfo }) {
|
|
264
|
+
console.log(`[Agent] Starting task: ${taskInput} on ${targetUrl} (Session: ${sessionId})`);
|
|
265
|
+
const screenshotsDir = path2.join(process.cwd(), "screenshots");
|
|
266
|
+
if (!fs.existsSync(screenshotsDir)) {
|
|
267
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
268
|
+
console.log(`\u{1F4C1} \u521B\u5EFA\u622A\u56FE\u76EE\u5F55: ${screenshotsDir}`);
|
|
269
|
+
}
|
|
270
|
+
const browser = await chromium.launch({
|
|
271
|
+
headless: true,
|
|
272
|
+
// Visible for demo/debug
|
|
273
|
+
args: ["--start-maximized"]
|
|
274
|
+
// Attempt to maximize
|
|
275
|
+
});
|
|
276
|
+
const context = await browser.newContext({
|
|
277
|
+
viewport: { width: 430, height: 800 }
|
|
278
|
+
// Set a reasonable fixed viewport
|
|
279
|
+
});
|
|
280
|
+
const page = await context.newPage();
|
|
281
|
+
let conversationHistory = [];
|
|
282
|
+
try {
|
|
283
|
+
let finalUrl = targetUrl;
|
|
284
|
+
if (!finalUrl.startsWith("http://") && !finalUrl.startsWith("https://")) {
|
|
285
|
+
finalUrl = "http://" + finalUrl;
|
|
286
|
+
}
|
|
287
|
+
const urlObj = new URL(finalUrl);
|
|
288
|
+
urlObj.searchParams.append("sessionId", sessionId);
|
|
289
|
+
finalUrl = urlObj.toString();
|
|
290
|
+
console.log(`Navigating to ${finalUrl}...`);
|
|
291
|
+
await page.goto(finalUrl, { waitUntil: "load" });
|
|
292
|
+
await page.waitForTimeout(2e3);
|
|
293
|
+
let isRunning = true;
|
|
294
|
+
let loopCount = 0;
|
|
295
|
+
const MAX_LOOPS = 20;
|
|
296
|
+
while (isRunning && loopCount < MAX_LOOPS) {
|
|
297
|
+
loopCount++;
|
|
298
|
+
console.log(`
|
|
299
|
+
--- Cycle ${loopCount} ---`);
|
|
300
|
+
const screenshotBuffer = await page.screenshot({ format: "png" });
|
|
301
|
+
const screenshotBase64 = screenshotBuffer.toString("base64");
|
|
302
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
303
|
+
const screenshotPath = path2.join(screenshotsDir, `screenshot_${sessionId}_${loopCount}_${timestamp}.png`);
|
|
304
|
+
fs.writeFileSync(screenshotPath, screenshotBuffer);
|
|
305
|
+
console.log(`\u{1F4F8} \u622A\u56FE\u5DF2\u4FDD\u5B58: ${screenshotPath}`);
|
|
306
|
+
const viewport = page.viewportSize();
|
|
307
|
+
console.log(`\u{1F4D0} Viewport\u5C3A\u5BF8: ${viewport.width}x${viewport.height}`);
|
|
308
|
+
console.log(`\u{1F4E6} \u622A\u56FE\u5927\u5C0F: ${screenshotBuffer.length} bytes`);
|
|
309
|
+
let promptText = `Task: ${taskInput}
|
|
310
|
+
`;
|
|
311
|
+
if (simulatedUserKnownInfo) {
|
|
312
|
+
promptText += `
|
|
313
|
+
Known User Info: ${JSON.stringify(simulatedUserKnownInfo, null, 2)}
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
promptText += `
|
|
317
|
+
Please perform the next action based on the screenshot.`;
|
|
318
|
+
const userContent = [
|
|
319
|
+
{ type: "text", text: promptText },
|
|
320
|
+
{ type: "image", image: screenshotBase64 }
|
|
321
|
+
];
|
|
322
|
+
conversationHistory.push({
|
|
323
|
+
role: "user",
|
|
324
|
+
content: userContent
|
|
325
|
+
});
|
|
326
|
+
console.log("Querying AI...");
|
|
327
|
+
const aiContent = await queryAI(conversationHistory);
|
|
328
|
+
if (!aiContent) {
|
|
329
|
+
console.error("Invalid AI response. Retrying...");
|
|
330
|
+
await page.waitForTimeout(2e3);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
conversationHistory.push({
|
|
334
|
+
role: "assistant",
|
|
335
|
+
content: aiContent
|
|
336
|
+
});
|
|
337
|
+
console.log("Raw AI Content (Excerpt):", aiContent);
|
|
338
|
+
const actions = parseActions(aiContent);
|
|
339
|
+
if (actions.length === 0) {
|
|
340
|
+
console.log("No actions parsed from AI response.");
|
|
341
|
+
}
|
|
342
|
+
console.log("Actions:", actions);
|
|
343
|
+
for (const action of actions) {
|
|
344
|
+
const result = await executeAction(page, action);
|
|
345
|
+
console.log("Action result:", result);
|
|
346
|
+
if (result === "FINISHED") {
|
|
347
|
+
isRunning = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (isRunning) {
|
|
352
|
+
await page.waitForTimeout(3e3);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { status: "success", message: "Task completed", messages: conversationHistory };
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error("Runtime Custom Error:", error);
|
|
358
|
+
return { status: "error", message: error.message };
|
|
359
|
+
} finally {
|
|
360
|
+
console.log("Closing browser...");
|
|
361
|
+
await browser.close();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/index.js
|
|
366
|
+
import AdmZip from "adm-zip";
|
|
367
|
+
import dotenv2 from "dotenv";
|
|
368
|
+
dotenv2.config();
|
|
369
|
+
function getFormattedDate() {
|
|
370
|
+
const now = /* @__PURE__ */ new Date();
|
|
371
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
372
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
373
|
+
}
|
|
374
|
+
async function runTaskLoop(targetWeb, targetServer, taskId) {
|
|
375
|
+
const results = [];
|
|
376
|
+
const datetime = getFormattedDate();
|
|
377
|
+
const folderName = `${taskId}_${datetime}`;
|
|
378
|
+
const outputDir = path3.join(process.cwd(), folderName);
|
|
379
|
+
if (!fs2.existsSync(outputDir)) {
|
|
380
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
381
|
+
console.log(`Created output directory: ${outputDir}`);
|
|
382
|
+
}
|
|
383
|
+
if (!targetWeb.startsWith("http")) targetWeb = "http://" + targetWeb;
|
|
384
|
+
if (!targetServer.startsWith("http")) targetServer = "http://" + targetServer;
|
|
385
|
+
console.log(`Starting Task Loop for ${taskId}`);
|
|
386
|
+
for (let i = 1; i <= 2; i++) {
|
|
387
|
+
console.log(`
|
|
388
|
+
=== Starting Iteration ${i}/5 ===`);
|
|
389
|
+
try {
|
|
390
|
+
const data = await executeSingleCycle(targetWeb, targetServer, taskId, i);
|
|
391
|
+
const { sessionId, agentMessages, ...rest } = data;
|
|
392
|
+
const sessionDir = path3.join(outputDir, sessionId);
|
|
393
|
+
if (!fs2.existsSync(sessionDir)) {
|
|
394
|
+
fs2.mkdirSync(sessionDir, { recursive: true });
|
|
395
|
+
}
|
|
396
|
+
fs2.writeFileSync(
|
|
397
|
+
path3.join(sessionDir, "messages.json"),
|
|
398
|
+
JSON.stringify(agentMessages, null, 2)
|
|
399
|
+
);
|
|
400
|
+
fs2.writeFileSync(
|
|
401
|
+
path3.join(sessionDir, "result.json"),
|
|
402
|
+
JSON.stringify(rest, null, 2)
|
|
403
|
+
);
|
|
404
|
+
results.push({ sessionId, ...rest });
|
|
405
|
+
console.log(`Iteration ${i} completed. Saved to ${sessionDir}`);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.error(`Iteration ${i} failed:`, e.stack || e.message);
|
|
408
|
+
results.push({ iteration: i, error: e.message });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const finalResultPath = path3.join(outputDir, "final-results.json");
|
|
412
|
+
fs2.writeFileSync(finalResultPath, JSON.stringify(results, null, 2));
|
|
413
|
+
console.log(`Saved consolidated results to ${finalResultPath}`);
|
|
414
|
+
console.log("Packaging results into ZIP...");
|
|
415
|
+
const zipName = `${folderName}.zip`;
|
|
416
|
+
const zipPath = path3.join(process.cwd(), zipName);
|
|
417
|
+
try {
|
|
418
|
+
const zip = new AdmZip();
|
|
419
|
+
zip.addLocalFolder(outputDir, folderName);
|
|
420
|
+
zip.writeZip(zipPath);
|
|
421
|
+
console.log(`
|
|
422
|
+
\u2705 All done! Zip saved to: ${zipPath}`);
|
|
423
|
+
} catch (zipError) {
|
|
424
|
+
console.error("Failed to create zip:", zipError);
|
|
425
|
+
}
|
|
426
|
+
return zipPath;
|
|
427
|
+
}
|
|
428
|
+
async function executeSingleCycle(targetWeb, targetServer, taskId, iteration) {
|
|
429
|
+
const fetchJson = async (url, opts) => {
|
|
430
|
+
const res = await fetch(url, opts);
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
const text = await res.text();
|
|
433
|
+
throw new Error(`API ${url} failed: ${res.status} ${text}`);
|
|
434
|
+
}
|
|
435
|
+
return await res.json();
|
|
436
|
+
};
|
|
437
|
+
console.log("Calling Start API...");
|
|
438
|
+
const startUrl = `${targetServer}/api/tasks/${taskId}/start`;
|
|
439
|
+
const startData = await fetchJson(startUrl, { method: "POST" });
|
|
440
|
+
if (startData.code !== 200) {
|
|
441
|
+
throw new Error(`Start API failed: ${JSON.stringify(startData)}`);
|
|
442
|
+
}
|
|
443
|
+
const { session_id: sessionId, TaskJson } = startData.data;
|
|
444
|
+
console.log(`Setup for session ${sessionId}...`);
|
|
445
|
+
const setupUrl = `${targetServer}/api/tasks/setup`;
|
|
446
|
+
await fetchJson(setupUrl, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: { "Content-Type": "application/json" },
|
|
449
|
+
body: JSON.stringify({ task_id: taskId, session_id: sessionId })
|
|
450
|
+
});
|
|
451
|
+
console.log("Running Agent...");
|
|
452
|
+
const agentResult = await runAgent({
|
|
453
|
+
taskInput: TaskJson.task.instruction,
|
|
454
|
+
targetUrl: targetWeb,
|
|
455
|
+
sessionId,
|
|
456
|
+
simulatedUserKnownInfo: TaskJson.task.simulated_user_known_info
|
|
457
|
+
});
|
|
458
|
+
console.log("Verifying result...");
|
|
459
|
+
const verifyUrl = `${targetServer}/api/tasks/verify`;
|
|
460
|
+
const verifyData = await fetchJson(verifyUrl, {
|
|
461
|
+
method: "POST",
|
|
462
|
+
headers: { "Content-Type": "application/json" },
|
|
463
|
+
body: JSON.stringify({ task_id: taskId, session_id: sessionId })
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
iteration,
|
|
467
|
+
sessionId,
|
|
468
|
+
agentStatus: agentResult.status,
|
|
469
|
+
agentMessages: agentResult.messages || [],
|
|
470
|
+
verifyResult: verifyData.data
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
export {
|
|
474
|
+
runAgent,
|
|
475
|
+
runTaskLoop
|
|
476
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rl-simulator-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup src/index.js --format cjs,esm --dts --clean"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@ai-sdk/openai": "^3.0.14",
|
|
12
|
+
"adm-zip": "^0.5.10",
|
|
13
|
+
"ai": "^6.0.44",
|
|
14
|
+
"dotenv": "^17.2.3",
|
|
15
|
+
"playwright": "^1.57.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"tsup": "^8.0.0",
|
|
19
|
+
"typescript": "^5.9.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/actions.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
|
|
2
|
+
async function executeAction(page, action) {
|
|
3
|
+
console.log(`Executing action: ${action.name}`, action.params);
|
|
4
|
+
|
|
5
|
+
const { name, params } = action;
|
|
6
|
+
const viewport = page.viewportSize();
|
|
7
|
+
|
|
8
|
+
// Helper to transform 0-1000 coordinates to actual viewport size
|
|
9
|
+
const transformPoint = (pointStr) => {
|
|
10
|
+
if (!pointStr) return null;
|
|
11
|
+
const [x, y] = pointStr.split(' ').map(Number);
|
|
12
|
+
|
|
13
|
+
if (!viewport) return { x, y };
|
|
14
|
+
|
|
15
|
+
const actualX = (x / 1000) * viewport.width;
|
|
16
|
+
const actualY = (y / 1000) * viewport.height;
|
|
17
|
+
return { x: actualX, y: actualY };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
switch (name) {
|
|
22
|
+
case 'click':
|
|
23
|
+
if (params.point) {
|
|
24
|
+
const { x, y } = transformPoint(params.point);
|
|
25
|
+
console.log(`Executing click action: (${x}, ${y}) [Original: ${params.point}]`);
|
|
26
|
+
await page.mouse.click(x, y);
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case 'left_double':
|
|
31
|
+
if (params.point) {
|
|
32
|
+
const { x, y } = transformPoint(params.point);
|
|
33
|
+
await page.mouse.dblclick(x, y);
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
|
|
37
|
+
case 'left_triple':
|
|
38
|
+
if (params.point) {
|
|
39
|
+
const { x, y } = transformPoint(params.point);
|
|
40
|
+
await page.mouse.click(x, y, { clickCount: 3 });
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'right_single':
|
|
45
|
+
if (params.point) {
|
|
46
|
+
const { x, y } = transformPoint(params.point);
|
|
47
|
+
await page.mouse.click(x, y, { button: 'right' });
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'drag':
|
|
52
|
+
if (params.start_point && params.end_point) {
|
|
53
|
+
const start = transformPoint(params.start_point);
|
|
54
|
+
const end = transformPoint(params.end_point);
|
|
55
|
+
await page.mouse.move(start.x, start.y);
|
|
56
|
+
await page.mouse.down();
|
|
57
|
+
await page.mouse.move(end.x, end.y);
|
|
58
|
+
await page.mouse.up();
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'move_to':
|
|
63
|
+
if (params.point) {
|
|
64
|
+
const { x, y } = transformPoint(params.point);
|
|
65
|
+
await page.mouse.move(x, y);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'mouse_down':
|
|
70
|
+
if (params.point) {
|
|
71
|
+
const { x, y } = transformPoint(params.point);
|
|
72
|
+
await page.mouse.move(x, y);
|
|
73
|
+
await page.mouse.down({ button: params.button || 'left' });
|
|
74
|
+
} else {
|
|
75
|
+
await page.mouse.down({ button: params.button || 'left' });
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case 'mouse_up':
|
|
80
|
+
if (params.point) {
|
|
81
|
+
const { x, y } = transformPoint(params.point);
|
|
82
|
+
await page.mouse.move(x, y);
|
|
83
|
+
await page.mouse.up({ button: params.button || 'left' });
|
|
84
|
+
} else {
|
|
85
|
+
await page.mouse.up({ button: params.button || 'left' });
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'type':
|
|
90
|
+
if (params.content) {
|
|
91
|
+
const content = params.content;
|
|
92
|
+
await page.keyboard.type(content);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'hotkey':
|
|
97
|
+
if (params.key) {
|
|
98
|
+
const keys = params.key.split(' ').join('+');
|
|
99
|
+
await page.keyboard.press(keys);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'press':
|
|
104
|
+
if (params.key) {
|
|
105
|
+
await page.keyboard.press(params.key);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'scroll':
|
|
110
|
+
const direction = params.direction;
|
|
111
|
+
if (direction === 'down') {
|
|
112
|
+
await page.evaluate(() => window.scrollBy(0, 500));
|
|
113
|
+
} else if (direction === 'up') {
|
|
114
|
+
await page.evaluate(() => window.scrollBy(0, -500));
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'wait':
|
|
119
|
+
if (params.time) {
|
|
120
|
+
const ms = parseFloat(params.time) * 1000;
|
|
121
|
+
await page.waitForTimeout(ms);
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'finished':
|
|
126
|
+
console.log("AI Task Finished:", params.content);
|
|
127
|
+
return 'FINISHED';
|
|
128
|
+
|
|
129
|
+
case 'call_user':
|
|
130
|
+
console.log("AI requests user input:", params.content);
|
|
131
|
+
return 'FINISHED';
|
|
132
|
+
|
|
133
|
+
default:
|
|
134
|
+
console.warn(`Unknown action: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error(`Failed to execute action ${name}:`, e);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return 'CONTINUE';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
executeAction
|
|
145
|
+
};
|