Skip to content

Commit d1ea9db

Browse files
authored
Merge pull request #262 from FellouAI/develop
fix: Multi-environment compatibility
2 parents c01a87e + 9561859 commit d1ea9db

File tree

18 files changed

+245
-92
lines changed

18 files changed

+245
-92
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Follow these steps when moving an existing Eko 2.x project to 3.0:
7575
const llms: LLMs = {
7676
default: {
7777
provider: "anthropic",
78-
model: "claude-sonnet-4-20250514",
78+
model: "claude-sonnet-4-5-20250929",
7979
apiKey: "your-api-key"
8080
},
8181
gemini: {

example/nodejs/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
"eko"
1717
],
1818
"dependencies": {
19-
"playwright": "^1.52.0",
20-
"@eko-ai/eko": "file:../../packages/eko-core",
21-
"@eko-ai/eko-nodejs": "file:../../packages/eko-nodejs"
19+
"@eko-ai/eko": "workspace:*",
20+
"@eko-ai/eko-nodejs": "workspace:*",
21+
"canvas": "^3.2.0",
22+
"playwright": "^1.52.0"
2223
},
2324
"devDependencies": {
2425
"@rollup/plugin-commonjs": "^28.0.3",

example/nodejs/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ const claudeApiKey = process.env.ANTHROPIC_API_KEY;
1212
const llms: LLMs = {
1313
default: {
1414
provider: "anthropic",
15-
model: "claude-sonnet-4-20250514",
15+
model: "claude-sonnet-4-5-20250929",
1616
apiKey: claudeApiKey || "",
1717
config: {
1818
baseURL: claudeBaseURL,
1919
},
2020
},
2121
openai: {
2222
provider: "openai",
23-
model: "gpt-5-mini",
23+
model: "gpt-5",
2424
apiKey: openaiApiKey || "",
2525
config: {
2626
baseURL: openaiBaseURL,

example/web/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"react-dom": "^19.1.0",
1818
"react-scripts": "^5.0.1",
1919
"@react-login-page/base": "^1.0.4",
20-
"@eko-ai/eko": "file:../../packages/eko-core",
21-
"@eko-ai/eko-web": "file:../../packages/eko-web"
20+
"@eko-ai/eko": "workspace:*",
21+
"@eko-ai/eko-web": "workspace:*"
2222
},
2323
"browserslist": {
2424
"production": [

example/web/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export async function auto_test_case() {
66
const llms: LLMs = {
77
default: {
88
provider: "anthropic",
9-
model: "claude-sonnet-4-20250514",
9+
model: "claude-sonnet-4-5-20250929",
1010
apiKey: "your_api_key",
1111
config: {
1212
baseURL: "https://api.anthropic.com/v1",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eko-ai/eko",
3-
"version": "3.1.3",
3+
"version": "3.1.4",
44
"description": "Empowering language to transform human words into action.",
55
"workspaces": [
66
"packages/eko-core",

packages/eko-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eko-ai/eko",
3-
"version": "3.1.3-alpha.1",
3+
"version": "3.1.4",
44
"description": "Empowering language to transform human words into action.",
55
"main": "dist/index.cjs.js",
66
"module": "dist/index.esm.js",

packages/eko-core/rollup.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default [
1313
sourcemap: true
1414
}
1515
],
16-
external: ['dotenv'],
16+
external: ['dotenv', 'buffer', 'canvas'],
1717
plugins: [
1818
commonjs(),
1919
resolve({
@@ -36,7 +36,7 @@ export default [
3636
sourcemap: true
3737
}
3838
],
39-
external: ['dotenv', 'buffer'],
39+
external: ['dotenv', 'buffer', 'canvas'],
4040
plugins: [
4141
commonjs(),
4242
resolve({

packages/eko-core/src/agent/browser/utils.ts

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { loadPackage } from "../../common/utils";
2+
13
export function extract_page_content(
24
max_url_length = 200,
35
max_content_length = 50000
@@ -131,30 +133,102 @@ export function mark_screenshot_highlight_elements(
131133
): Promise<string> {
132134
return new Promise<string>(async (resolve, reject) => {
133135
try {
134-
// Convert base64 to Blob
135-
const base64Data = screenshot.imageBase64;
136-
const binaryString = atob(base64Data);
137-
const bytes = new Uint8Array(binaryString.length);
138-
for (let i = 0; i < binaryString.length; i++) {
139-
bytes[i] = binaryString.charCodeAt(i);
140-
}
141-
const blob = new Blob([bytes], { type: screenshot.imageType });
142-
const imageBitmap = await createImageBitmap(blob, {
143-
resizeQuality: "high",
144-
resizeWidth: client_rect.width,
145-
resizeHeight: client_rect.height,
146-
});
147-
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
136+
const hasOffscreen = typeof OffscreenCanvas !== "undefined";
137+
const hasCreateImageBitmap = typeof createImageBitmap !== "undefined";
138+
const hasDOM = typeof document !== "undefined" && typeof Image !== "undefined";
139+
// @ts-ignore
140+
const isNode = typeof window === "undefined" && typeof process !== "undefined" && !!process.versions && !!process.versions.node;
141+
142+
const loadImageAny = async () => {
143+
if (hasCreateImageBitmap) {
144+
const base64Data = screenshot.imageBase64;
145+
const binaryString = atob(base64Data);
146+
const bytes = new Uint8Array(binaryString.length);
147+
for (let i = 0; i < binaryString.length; i++) {
148+
bytes[i] = binaryString.charCodeAt(i);
149+
}
150+
const blob = new Blob([bytes], { type: screenshot.imageType });
151+
const imageBitmap = await createImageBitmap(blob, {
152+
resizeQuality: "high",
153+
resizeWidth: client_rect.width,
154+
resizeHeight: client_rect.height,
155+
} as any);
156+
return { img: imageBitmap };
157+
}
158+
if (hasDOM) {
159+
const img = await new Promise<HTMLImageElement>(
160+
(resolveImg, rejectImg) => {
161+
const image = new Image();
162+
image.onload = () => resolveImg(image);
163+
image.onerror = (e) => rejectImg(e);
164+
image.src = `data:${screenshot.imageType};base64,${screenshot.imageBase64}`;
165+
}
166+
);
167+
return { img };
168+
}
169+
if (isNode) {
170+
const canvasMod = await loadPackage("canvas");
171+
const { loadImage } = canvasMod as any;
172+
const dataUrl = `data:${screenshot.imageType};base64,${screenshot.imageBase64}`;
173+
const img = await loadImage(dataUrl);
174+
return { img };
175+
}
176+
throw new Error("No image environment available");
177+
};
178+
179+
const createCanvasAny = async (width: number, height: number) => {
180+
if (hasOffscreen) {
181+
const canvas = new OffscreenCanvas(width, height) as any;
182+
return {
183+
ctx: canvas.getContext("2d") as any,
184+
exportDataUrl: async (mime: string) => {
185+
const blob = await canvas.convertToBlob({ type: mime });
186+
return await new Promise<string>((res, rej) => {
187+
const reader = new FileReader();
188+
reader.onloadend = () => res(reader.result as string);
189+
reader.onerror = () =>
190+
rej(new Error("Failed to convert blob to base64"));
191+
reader.readAsDataURL(blob);
192+
});
193+
},
194+
};
195+
}
196+
if (hasDOM) {
197+
const canvas = document.createElement("canvas");
198+
canvas.width = width;
199+
canvas.height = height;
200+
return {
201+
ctx: canvas.getContext("2d") as any,
202+
exportDataUrl: async (mime: string) => canvas.toDataURL(mime),
203+
};
204+
}
205+
if (isNode) {
206+
const canvasMod = await loadPackage("canvas");
207+
const { createCanvas } = canvasMod as any;
208+
const canvas = createCanvas(width, height);
209+
return {
210+
ctx: canvas.getContext("2d"),
211+
exportDataUrl: async (mime: string) => canvas.toDataURL(mime),
212+
};
213+
}
214+
throw new Error("No canvas environment available");
215+
};
148216

149-
const ctx = canvas.getContext("2d");
217+
const loaded = await loadImageAny();
218+
const targetWidth = client_rect.width;
219+
const targetHeight = client_rect.height;
220+
const { ctx, exportDataUrl } = await createCanvasAny(
221+
targetWidth,
222+
targetHeight
223+
);
150224
if (!ctx) {
151225
reject(new Error("Failed to get canvas context"));
152226
return;
153227
}
154228

155229
ctx.imageSmoothingEnabled = true;
156230
ctx.imageSmoothingQuality = "high";
157-
ctx.drawImage(imageBitmap, 0, 0);
231+
ctx.drawImage(loaded.img, 0, 0, targetWidth, targetHeight);
158232

159233
const sortedEntries = Object.entries(area_map)
160234
.filter(([id, area]) => area.width > 0 && area.height > 0)
@@ -163,7 +237,7 @@ export function mark_screenshot_highlight_elements(
163237
const areaB = b[1].width * b[1].height;
164238
return areaB - areaA;
165239
});
166-
240+
167241
const colors = [
168242
"#FF0000",
169243
"#00FF00",
@@ -178,10 +252,8 @@ export function mark_screenshot_highlight_elements(
178252
"#DC143C",
179253
"#4682B4",
180254
];
181-
182255
sortedEntries.forEach(([id, area], index) => {
183256
const color = colors[index % colors.length];
184-
185257
if (area.width * area.height < 40000) {
186258
// Draw a background color
187259
ctx.fillStyle = color + "1A";
@@ -196,9 +268,10 @@ export function mark_screenshot_highlight_elements(
196268
// Draw ID tag background
197269
const fontSize = Math.min(12, Math.max(8, area.height / 2));
198270
ctx.font = `${fontSize}px sans-serif`;
199-
const textMetrics = ctx.measureText(id);
271+
const metrics: any = ctx.measureText(id) as any;
272+
const textWidth = metrics && metrics.width ? metrics.width : 0;
200273
const padding = 4;
201-
const labelWidth = textMetrics.width + padding * 2;
274+
const labelWidth = textWidth + padding * 2;
202275
const labelHeight = fontSize + padding * 2;
203276

204277
// The tag position is in the upper right corner.
@@ -221,20 +294,9 @@ export function mark_screenshot_highlight_elements(
221294
ctx.fillText(id, labelX + padding, labelY + padding);
222295
});
223296

224-
// Convert OffscreenCanvas to Blob, then to base64
225-
const resultBlob = await canvas.convertToBlob({
226-
type: screenshot.imageType,
227-
});
228-
229-
const reader = new FileReader();
230-
reader.onloadend = () => {
231-
const resultBase64 = reader.result as string;
232-
resolve(resultBase64);
233-
};
234-
reader.onerror = () => {
235-
reject(new Error("Failed to convert blob to base64"));
236-
};
237-
reader.readAsDataURL(resultBlob);
297+
// Export the image
298+
const out = await exportDataUrl(screenshot.imageType);
299+
resolve(out);
238300
} catch (error) {
239301
reject(error);
240302
}

0 commit comments

Comments
 (0)