This is page 3 of 10. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── constants.ts
│ │ │ ├── message-types.ts
│ │ │ └── tool-handler.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── index.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ └── tools
│ │ │ │ ├── base-browser.ts
│ │ │ │ ├── browser
│ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ ├── interaction.ts
│ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ ├── network-request.ts
│ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ └── window.ts
│ │ │ │ └── index.ts
│ │ │ ├── content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ └── popup
│ │ │ ├── App.vue
│ │ │ ├── components
│ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ ├── icons
│ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ └── VectorIcon.vue
│ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ └── ProgressIndicator.vue
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ └── style.css
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── click-helper.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── screenshot-helper.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ └── server.test.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│ ├── chrome-extension
│ │ └── latest
│ │ └── chrome-mcp-server-lastest.zip
│ └── README.md
└── test-inject-script.js
```
# Files
--------------------------------------------------------------------------------
/app/native-server/src/server/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2 | import cors from '@fastify/cors';
3 | import {
4 | NATIVE_SERVER_PORT,
5 | TIMEOUTS,
6 | SERVER_CONFIG,
7 | HTTP_STATUS,
8 | ERROR_MESSAGES,
9 | } from '../constant';
10 | import { NativeMessagingHost } from '../native-messaging-host';
11 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
12 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
13 | import { randomUUID } from 'node:crypto';
14 | import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
15 | import { getMcpServer } from '../mcp/mcp-server';
16 |
17 | // Define request body type (if data needs to be retrieved from HTTP requests)
18 | interface ExtensionRequestPayload {
19 | data?: any; // Data you want to pass to the extension
20 | }
21 |
22 | export class Server {
23 | private fastify: FastifyInstance;
24 | public isRunning = false; // Changed to public or provide a getter
25 | private nativeHost: NativeMessagingHost | null = null;
26 | private transportsMap: Map<string, StreamableHTTPServerTransport | SSEServerTransport> =
27 | new Map();
28 |
29 | constructor() {
30 | this.fastify = Fastify({ logger: SERVER_CONFIG.LOGGER_ENABLED });
31 | this.setupPlugins();
32 | this.setupRoutes();
33 | }
34 | /**
35 | * Associate NativeMessagingHost instance
36 | */
37 | public setNativeHost(nativeHost: NativeMessagingHost): void {
38 | this.nativeHost = nativeHost;
39 | }
40 |
41 | private async setupPlugins(): Promise<void> {
42 | await this.fastify.register(cors, {
43 | origin: SERVER_CONFIG.CORS_ORIGIN,
44 | });
45 | }
46 |
47 | private setupRoutes(): void {
48 | // for ping
49 | this.fastify.get(
50 | '/ask-extension',
51 | async (request: FastifyRequest<{ Body: ExtensionRequestPayload }>, reply: FastifyReply) => {
52 |
53 | if (!this.nativeHost) {
54 | return reply
55 | .status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
56 | .send({ error: ERROR_MESSAGES.NATIVE_HOST_NOT_AVAILABLE });
57 | }
58 | if (!this.isRunning) {
59 | return reply
60 | .status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
61 | .send({ error: ERROR_MESSAGES.SERVER_NOT_RUNNING });
62 | }
63 |
64 | try {
65 | // wait from extension message
66 | const extensionResponse = await this.nativeHost.sendRequestToExtensionAndWait(
67 | request.query,
68 | 'process_data',
69 | TIMEOUTS.EXTENSION_REQUEST_TIMEOUT,
70 | );
71 | return reply.status(HTTP_STATUS.OK).send({ status: 'success', data: extensionResponse });
72 | } catch (error: any) {
73 | if (error.message.includes('timed out')) {
74 | return reply
75 | .status(HTTP_STATUS.GATEWAY_TIMEOUT)
76 | .send({ status: 'error', message: ERROR_MESSAGES.REQUEST_TIMEOUT });
77 | } else {
78 | return reply.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
79 | status: 'error',
80 | message: `Failed to get response from extension: ${error.message}`,
81 | });
82 | }
83 | }
84 | },
85 | );
86 |
87 | // Compatible with SSE
88 | this.fastify.get('/sse', async (_, reply) => {
89 | try {
90 | // Set SSE headers
91 | reply.raw.writeHead(HTTP_STATUS.OK, {
92 | 'Content-Type': 'text/event-stream',
93 | 'Cache-Control': 'no-cache',
94 | Connection: 'keep-alive',
95 | });
96 |
97 | // Create SSE transport
98 | const transport = new SSEServerTransport('/messages', reply.raw);
99 | this.transportsMap.set(transport.sessionId, transport);
100 |
101 | reply.raw.on('close', () => {
102 | this.transportsMap.delete(transport.sessionId);
103 | });
104 |
105 | const server = getMcpServer();
106 | await server.connect(transport);
107 |
108 | // Keep connection open
109 | reply.raw.write(':\n\n');
110 | } catch (error) {
111 | if (!reply.sent) {
112 | reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
113 | }
114 | }
115 | });
116 |
117 | // Compatible with SSE
118 | this.fastify.post('/messages', async (req, reply) => {
119 | try {
120 | const { sessionId } = req.query as any;
121 | const transport = this.transportsMap.get(sessionId) as SSEServerTransport;
122 | if (!sessionId || !transport) {
123 | reply.code(HTTP_STATUS.BAD_REQUEST).send('No transport found for sessionId');
124 | return;
125 | }
126 |
127 | await transport.handlePostMessage(req.raw, reply.raw, req.body);
128 | } catch (error) {
129 | if (!reply.sent) {
130 | reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
131 | }
132 | }
133 | });
134 |
135 | // POST /mcp: Handle client-to-server messages
136 | this.fastify.post('/mcp', async (request, reply) => {
137 | const sessionId = request.headers['mcp-session-id'] as string | undefined;
138 | let transport: StreamableHTTPServerTransport | undefined = this.transportsMap.get(
139 | sessionId || '',
140 | ) as StreamableHTTPServerTransport;
141 | if (transport) {
142 | // transport found, do nothing
143 | } else if (!sessionId && isInitializeRequest(request.body)) {
144 | const newSessionId = randomUUID(); // Generate session ID
145 | transport = new StreamableHTTPServerTransport({
146 | sessionIdGenerator: () => newSessionId, // Use pre-generated ID
147 | onsessioninitialized: (initializedSessionId) => {
148 | // Ensure transport instance exists and session ID matches
149 | if (transport && initializedSessionId === newSessionId) {
150 | this.transportsMap.set(initializedSessionId, transport);
151 | }
152 | },
153 | });
154 |
155 | transport.onclose = () => {
156 | if (transport?.sessionId && this.transportsMap.get(transport.sessionId)) {
157 | this.transportsMap.delete(transport.sessionId);
158 | }
159 | };
160 | await getMcpServer().connect(transport);
161 | } else {
162 | reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_MCP_REQUEST });
163 | return;
164 | }
165 |
166 | try {
167 | await transport.handleRequest(request.raw, reply.raw, request.body);
168 | } catch (error) {
169 | if (!reply.sent) {
170 | reply
171 | .code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
172 | .send({ error: ERROR_MESSAGES.MCP_REQUEST_PROCESSING_ERROR });
173 | }
174 | }
175 | });
176 |
177 | this.fastify.get('/mcp', async (request, reply) => {
178 | const sessionId = request.headers['mcp-session-id'] as string | undefined;
179 | const transport = sessionId
180 | ? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
181 | : undefined;
182 | if (!transport) {
183 | reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SSE_SESSION });
184 | return;
185 | }
186 |
187 | reply.raw.setHeader('Content-Type', 'text/event-stream');
188 | reply.raw.setHeader('Cache-Control', 'no-cache');
189 | reply.raw.setHeader('Connection', 'keep-alive');
190 | reply.raw.flushHeaders(); // Ensure headers are sent immediately
191 |
192 | try {
193 | // transport.handleRequest will take over the response stream
194 | await transport.handleRequest(request.raw, reply.raw);
195 | if (!reply.sent) {
196 | // If transport didn't send anything (unlikely for SSE initial handshake)
197 | reply.hijack(); // Prevent Fastify from automatically sending response
198 | }
199 | } catch (error) {
200 | if (!reply.raw.writableEnded) {
201 | reply.raw.end();
202 | }
203 | }
204 |
205 | request.socket.on('close', () => {
206 | request.log.info(`SSE client disconnected for session: ${sessionId}`);
207 | // transport's onclose should handle its own cleanup
208 | });
209 | });
210 |
211 | this.fastify.delete('/mcp', async (request, reply) => {
212 | const sessionId = request.headers['mcp-session-id'] as string | undefined;
213 | const transport = sessionId
214 | ? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
215 | : undefined;
216 |
217 | if (!transport) {
218 | reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SESSION_ID });
219 | return;
220 | }
221 |
222 | try {
223 | await transport.handleRequest(request.raw, reply.raw);
224 | // Assume transport.handleRequest will send response or transport.onclose will cleanup
225 | if (!reply.sent) {
226 | reply.code(HTTP_STATUS.NO_CONTENT).send();
227 | }
228 | } catch (error) {
229 | if (!reply.sent) {
230 | reply
231 | .code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
232 | .send({ error: ERROR_MESSAGES.MCP_SESSION_DELETION_ERROR });
233 | }
234 | }
235 | });
236 | }
237 |
238 | public async start(port = NATIVE_SERVER_PORT, nativeHost: NativeMessagingHost): Promise<void> {
239 | if (!this.nativeHost) {
240 | this.nativeHost = nativeHost; // Ensure nativeHost is set
241 | } else if (this.nativeHost !== nativeHost) {
242 | this.nativeHost = nativeHost; // Update to the passed instance
243 | }
244 |
245 | if (this.isRunning) {
246 | return;
247 | }
248 |
249 | try {
250 | await this.fastify.listen({ port, host: SERVER_CONFIG.HOST });
251 | this.isRunning = true; // Update running status
252 | // No need to return, Promise resolves void by default
253 | } catch (err) {
254 | this.isRunning = false; // Startup failed, reset status
255 | // Throw error instead of exiting directly, let caller (possibly NativeHost) handle
256 | throw err; // or return Promise.reject(err);
257 | // process.exit(1); // Not recommended to exit directly here
258 | }
259 | }
260 |
261 | public async stop(): Promise<void> {
262 | if (!this.isRunning) {
263 | return;
264 | }
265 | // this.nativeHost = null; // Not recommended to nullify here, association relationship may still be needed
266 | try {
267 | await this.fastify.close();
268 | this.isRunning = false; // Update running status
269 | } catch (err) {
270 | // Even if closing fails, mark as not running, but log the error
271 | this.isRunning = false;
272 | throw err; // Throw error
273 | }
274 | }
275 |
276 | public getInstance(): FastifyInstance {
277 | return this.fastify;
278 | }
279 | }
280 |
281 | const serverInstance = new Server();
282 | export default serverInstance;
283 |
```
--------------------------------------------------------------------------------
/docs/TOOLS.md:
--------------------------------------------------------------------------------
```markdown
1 | # Chrome MCP Server API Reference 📚
2 |
3 | Complete reference for all available tools and their parameters.
4 |
5 | ## 📋 Table of Contents
6 |
7 | - [Browser Management](#browser-management)
8 | - [Screenshots & Visual](#screenshots--visual)
9 | - [Network Monitoring](#network-monitoring)
10 | - [Content Analysis](#content-analysis)
11 | - [Interaction](#interaction)
12 | - [Data Management](#data-management)
13 | - [Response Format](#response-format)
14 |
15 | ## 📊 Browser Management
16 |
17 | ### `get_windows_and_tabs`
18 |
19 | List all currently open browser windows and tabs.
20 |
21 | **Parameters**: None
22 |
23 | **Response**:
24 |
25 | ```json
26 | {
27 | "windowCount": 2,
28 | "tabCount": 5,
29 | "windows": [
30 | {
31 | "windowId": 123,
32 | "tabs": [
33 | {
34 | "tabId": 456,
35 | "url": "https://example.com",
36 | "title": "Example Page",
37 | "active": true
38 | }
39 | ]
40 | }
41 | ]
42 | }
43 | ```
44 |
45 | ### `chrome_navigate`
46 |
47 | Navigate to a URL with optional viewport control.
48 |
49 | **Parameters**:
50 |
51 | - `url` (string, required): URL to navigate to
52 | - `newWindow` (boolean, optional): Create new window (default: false)
53 | - `width` (number, optional): Viewport width in pixels (default: 1280)
54 | - `height` (number, optional): Viewport height in pixels (default: 720)
55 |
56 | **Example**:
57 |
58 | ```json
59 | {
60 | "url": "https://example.com",
61 | "newWindow": true,
62 | "width": 1920,
63 | "height": 1080
64 | }
65 | ```
66 |
67 | ### `chrome_close_tabs`
68 |
69 | Close specific tabs or windows.
70 |
71 | **Parameters**:
72 |
73 | - `tabIds` (array, optional): Array of tab IDs to close
74 | - `windowIds` (array, optional): Array of window IDs to close
75 |
76 | **Example**:
77 |
78 | ```json
79 | {
80 | "tabIds": [123, 456],
81 | "windowIds": [789]
82 | }
83 | ```
84 |
85 | ### `chrome_switch_tab`
86 |
87 | Switch to a specific browser tab.
88 |
89 | **Parameters**:
90 |
91 | - `tabId` (number, required): The ID of the tab to switch to.
92 | - `windowId` (number, optional): The ID of the window where the tab is located.
93 |
94 | **Example**:
95 |
96 | ```json
97 | {
98 | "tabId": 456,
99 | "windowId": 123
100 | }
101 | ```
102 |
103 | ### `chrome_go_back_or_forward`
104 |
105 | Navigate browser history.
106 |
107 | **Parameters**:
108 |
109 | - `direction` (string, required): "back" or "forward"
110 | - `tabId` (number, optional): Specific tab ID (default: active tab)
111 |
112 | **Example**:
113 |
114 | ```json
115 | {
116 | "direction": "back",
117 | "tabId": 123
118 | }
119 | ```
120 |
121 | ## 📸 Screenshots & Visual
122 |
123 | ### `chrome_screenshot`
124 |
125 | Take advanced screenshots with various options.
126 |
127 | **Parameters**:
128 |
129 | - `name` (string, optional): Screenshot filename
130 | - `selector` (string, optional): CSS selector for element screenshot
131 | - `width` (number, optional): Width in pixels (default: 800)
132 | - `height` (number, optional): Height in pixels (default: 600)
133 | - `storeBase64` (boolean, optional): Return base64 data (default: false)
134 | - `fullPage` (boolean, optional): Capture full page (default: true)
135 |
136 | **Example**:
137 |
138 | ```json
139 | {
140 | "selector": ".main-content",
141 | "fullPage": true,
142 | "storeBase64": true,
143 | "width": 1920,
144 | "height": 1080
145 | }
146 | ```
147 |
148 | **Response**:
149 |
150 | ```json
151 | {
152 | "success": true,
153 | "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
154 | "dimensions": {
155 | "width": 1920,
156 | "height": 1080
157 | }
158 | }
159 | ```
160 |
161 | ## 🌐 Network Monitoring
162 |
163 | ### `chrome_network_capture_start`
164 |
165 | Start capturing network requests using webRequest API.
166 |
167 | **Parameters**:
168 |
169 | - `url` (string, optional): URL to navigate to and capture
170 | - `maxCaptureTime` (number, optional): Maximum capture time in ms (default: 30000)
171 | - `inactivityTimeout` (number, optional): Stop after inactivity in ms (default: 3000)
172 | - `includeStatic` (boolean, optional): Include static resources (default: false)
173 |
174 | **Example**:
175 |
176 | ```json
177 | {
178 | "url": "https://api.example.com",
179 | "maxCaptureTime": 60000,
180 | "includeStatic": false
181 | }
182 | ```
183 |
184 | ### `chrome_network_capture_stop`
185 |
186 | Stop network capture and return collected data.
187 |
188 | **Parameters**: None
189 |
190 | **Response**:
191 |
192 | ```json
193 | {
194 | "success": true,
195 | "capturedRequests": [
196 | {
197 | "url": "https://api.example.com/data",
198 | "method": "GET",
199 | "status": 200,
200 | "requestHeaders": {...},
201 | "responseHeaders": {...},
202 | "responseTime": 150
203 | }
204 | ],
205 | "summary": {
206 | "totalRequests": 15,
207 | "captureTime": 5000
208 | }
209 | }
210 | ```
211 |
212 | ### `chrome_network_debugger_start`
213 |
214 | Start capturing with Chrome Debugger API (includes response bodies).
215 |
216 | **Parameters**:
217 |
218 | - `url` (string, optional): URL to navigate to and capture
219 |
220 | ### `chrome_network_debugger_stop`
221 |
222 | Stop debugger capture and return data with response bodies.
223 |
224 | ### `chrome_network_request`
225 |
226 | Send custom HTTP requests.
227 |
228 | **Parameters**:
229 |
230 | - `url` (string, required): Request URL
231 | - `method` (string, optional): HTTP method (default: "GET")
232 | - `headers` (object, optional): Request headers
233 | - `body` (string, optional): Request body
234 |
235 | **Example**:
236 |
237 | ```json
238 | {
239 | "url": "https://api.example.com/data",
240 | "method": "POST",
241 | "headers": {
242 | "Content-Type": "application/json"
243 | },
244 | "body": "{\"key\": \"value\"}"
245 | }
246 | ```
247 |
248 | ## 🔍 Content Analysis
249 |
250 | ### `search_tabs_content`
251 |
252 | AI-powered semantic search across browser tabs.
253 |
254 | **Parameters**:
255 |
256 | - `query` (string, required): Search query
257 |
258 | **Example**:
259 |
260 | ```json
261 | {
262 | "query": "machine learning tutorials"
263 | }
264 | ```
265 |
266 | **Response**:
267 |
268 | ```json
269 | {
270 | "success": true,
271 | "totalTabsSearched": 10,
272 | "matchedTabsCount": 3,
273 | "vectorSearchEnabled": true,
274 | "indexStats": {
275 | "totalDocuments": 150,
276 | "totalTabs": 10,
277 | "semanticEngineReady": true
278 | },
279 | "matchedTabs": [
280 | {
281 | "tabId": 123,
282 | "url": "https://example.com/ml-tutorial",
283 | "title": "Machine Learning Tutorial",
284 | "semanticScore": 0.85,
285 | "matchedSnippets": ["Introduction to machine learning..."],
286 | "chunkSource": "content"
287 | }
288 | ]
289 | }
290 | ```
291 |
292 | ### `chrome_get_web_content`
293 |
294 | Extract HTML or text content from web pages.
295 |
296 | **Parameters**:
297 |
298 | - `format` (string, optional): "html" or "text" (default: "text")
299 | - `selector` (string, optional): CSS selector for specific elements
300 | - `tabId` (number, optional): Specific tab ID (default: active tab)
301 |
302 | **Example**:
303 |
304 | ```json
305 | {
306 | "format": "text",
307 | "selector": ".article-content"
308 | }
309 | ```
310 |
311 | ### `chrome_get_interactive_elements`
312 |
313 | Find clickable and interactive elements on the page.
314 |
315 | **Parameters**:
316 |
317 | - `tabId` (number, optional): Specific tab ID (default: active tab)
318 |
319 | **Response**:
320 |
321 | ```json
322 | {
323 | "elements": [
324 | {
325 | "selector": "#submit-button",
326 | "type": "button",
327 | "text": "Submit",
328 | "visible": true,
329 | "clickable": true
330 | }
331 | ]
332 | }
333 | ```
334 |
335 | ## 🎯 Interaction
336 |
337 | ### `chrome_click_element`
338 |
339 | Click elements using CSS selectors.
340 |
341 | **Parameters**:
342 |
343 | - `selector` (string, required): CSS selector for target element
344 | - `tabId` (number, optional): Specific tab ID (default: active tab)
345 |
346 | **Example**:
347 |
348 | ```json
349 | {
350 | "selector": "#submit-button"
351 | }
352 | ```
353 |
354 | ### `chrome_fill_or_select`
355 |
356 | Fill form fields or select options.
357 |
358 | **Parameters**:
359 |
360 | - `selector` (string, required): CSS selector for target element
361 | - `value` (string, required): Value to fill or select
362 | - `tabId` (number, optional): Specific tab ID (default: active tab)
363 |
364 | **Example**:
365 |
366 | ```json
367 | {
368 | "selector": "#email-input",
369 | "value": "[email protected]"
370 | }
371 | ```
372 |
373 | ### `chrome_keyboard`
374 |
375 | Simulate keyboard input and shortcuts.
376 |
377 | **Parameters**:
378 |
379 | - `keys` (string, required): Key combination (e.g., "Ctrl+C", "Enter")
380 | - `selector` (string, optional): Target element selector
381 | - `delay` (number, optional): Delay between keystrokes in ms (default: 0)
382 |
383 | **Example**:
384 |
385 | ```json
386 | {
387 | "keys": "Ctrl+A",
388 | "selector": "#text-input",
389 | "delay": 100
390 | }
391 | ```
392 |
393 | ## 📚 Data Management
394 |
395 | ### `chrome_history`
396 |
397 | Search browser history with filters.
398 |
399 | **Parameters**:
400 |
401 | - `text` (string, optional): Search text in URL/title
402 | - `startTime` (string, optional): Start date (ISO format)
403 | - `endTime` (string, optional): End date (ISO format)
404 | - `maxResults` (number, optional): Maximum results (default: 100)
405 | - `excludeCurrentTabs` (boolean, optional): Exclude current tabs (default: true)
406 |
407 | **Example**:
408 |
409 | ```json
410 | {
411 | "text": "github",
412 | "startTime": "2024-01-01",
413 | "maxResults": 50
414 | }
415 | ```
416 |
417 | ### `chrome_bookmark_search`
418 |
419 | Search bookmarks by keywords.
420 |
421 | **Parameters**:
422 |
423 | - `query` (string, optional): Search keywords
424 | - `maxResults` (number, optional): Maximum results (default: 100)
425 | - `folderPath` (string, optional): Search within specific folder
426 |
427 | **Example**:
428 |
429 | ```json
430 | {
431 | "query": "documentation",
432 | "maxResults": 20,
433 | "folderPath": "Work/Resources"
434 | }
435 | ```
436 |
437 | ### `chrome_bookmark_add`
438 |
439 | Add new bookmarks with folder support.
440 |
441 | **Parameters**:
442 |
443 | - `url` (string, optional): URL to bookmark (default: current tab)
444 | - `title` (string, optional): Bookmark title (default: page title)
445 | - `parentId` (string, optional): Parent folder ID or path
446 | - `createFolder` (boolean, optional): Create folder if not exists (default: false)
447 |
448 | **Example**:
449 |
450 | ```json
451 | {
452 | "url": "https://example.com",
453 | "title": "Example Site",
454 | "parentId": "Work/Resources",
455 | "createFolder": true
456 | }
457 | ```
458 |
459 | ### `chrome_bookmark_delete`
460 |
461 | Delete bookmarks by ID or URL.
462 |
463 | **Parameters**:
464 |
465 | - `bookmarkId` (string, optional): Bookmark ID to delete
466 | - `url` (string, optional): URL to find and delete
467 |
468 | **Example**:
469 |
470 | ```json
471 | {
472 | "url": "https://example.com"
473 | }
474 | ```
475 |
476 | ## 📋 Response Format
477 |
478 | All tools return responses in the following format:
479 |
480 | ```json
481 | {
482 | "content": [
483 | {
484 | "type": "text",
485 | "text": "JSON string containing the actual response data"
486 | }
487 | ],
488 | "isError": false
489 | }
490 | ```
491 |
492 | For errors:
493 |
494 | ```json
495 | {
496 | "content": [
497 | {
498 | "type": "text",
499 | "text": "Error message describing what went wrong"
500 | }
501 | ],
502 | "isError": true
503 | }
504 | ```
505 |
506 | ## 🔧 Usage Examples
507 |
508 | ### Complete Workflow Example
509 |
510 | ```javascript
511 | // 1. Navigate to a page
512 | await callTool('chrome_navigate', {
513 | url: 'https://example.com',
514 | });
515 |
516 | // 2. Take a screenshot
517 | const screenshot = await callTool('chrome_screenshot', {
518 | fullPage: true,
519 | storeBase64: true,
520 | });
521 |
522 | // 3. Start network monitoring
523 | await callTool('chrome_network_capture_start', {
524 | maxCaptureTime: 30000,
525 | });
526 |
527 | // 4. Interact with the page
528 | await callTool('chrome_click_element', {
529 | selector: '#load-data-button',
530 | });
531 |
532 | // 5. Search content semantically
533 | const searchResults = await callTool('search_tabs_content', {
534 | query: 'user data analysis',
535 | });
536 |
537 | // 6. Stop network capture
538 | const networkData = await callTool('chrome_network_capture_stop');
539 |
540 | // 7. Save bookmark
541 | await callTool('chrome_bookmark_add', {
542 | title: 'Data Analysis Page',
543 | parentId: 'Work/Analytics',
544 | });
545 | ```
546 |
547 | This API provides comprehensive browser automation capabilities with AI-enhanced content analysis and semantic search features.
548 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/workers/simd_math.js:
--------------------------------------------------------------------------------
```javascript
1 | let wasm;
2 |
3 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
4 |
5 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
6 |
7 | let cachedUint8ArrayMemory0 = null;
8 |
9 | function getUint8ArrayMemory0() {
10 | if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
11 | cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
12 | }
13 | return cachedUint8ArrayMemory0;
14 | }
15 |
16 | function getStringFromWasm0(ptr, len) {
17 | ptr = ptr >>> 0;
18 | return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
19 | }
20 |
21 | let WASM_VECTOR_LEN = 0;
22 |
23 | const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
24 |
25 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
26 | ? function (arg, view) {
27 | return cachedTextEncoder.encodeInto(arg, view);
28 | }
29 | : function (arg, view) {
30 | const buf = cachedTextEncoder.encode(arg);
31 | view.set(buf);
32 | return {
33 | read: arg.length,
34 | written: buf.length
35 | };
36 | });
37 |
38 | function passStringToWasm0(arg, malloc, realloc) {
39 |
40 | if (realloc === undefined) {
41 | const buf = cachedTextEncoder.encode(arg);
42 | const ptr = malloc(buf.length, 1) >>> 0;
43 | getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
44 | WASM_VECTOR_LEN = buf.length;
45 | return ptr;
46 | }
47 |
48 | let len = arg.length;
49 | let ptr = malloc(len, 1) >>> 0;
50 |
51 | const mem = getUint8ArrayMemory0();
52 |
53 | let offset = 0;
54 |
55 | for (; offset < len; offset++) {
56 | const code = arg.charCodeAt(offset);
57 | if (code > 0x7F) break;
58 | mem[ptr + offset] = code;
59 | }
60 |
61 | if (offset !== len) {
62 | if (offset !== 0) {
63 | arg = arg.slice(offset);
64 | }
65 | ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
66 | const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
67 | const ret = encodeString(arg, view);
68 |
69 | offset += ret.written;
70 | ptr = realloc(ptr, len, offset, 1) >>> 0;
71 | }
72 |
73 | WASM_VECTOR_LEN = offset;
74 | return ptr;
75 | }
76 |
77 | let cachedDataViewMemory0 = null;
78 |
79 | function getDataViewMemory0() {
80 | if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
81 | cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
82 | }
83 | return cachedDataViewMemory0;
84 | }
85 |
86 | export function main() {
87 | wasm.main();
88 | }
89 |
90 | let cachedFloat32ArrayMemory0 = null;
91 |
92 | function getFloat32ArrayMemory0() {
93 | if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
94 | cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
95 | }
96 | return cachedFloat32ArrayMemory0;
97 | }
98 |
99 | function passArrayF32ToWasm0(arg, malloc) {
100 | const ptr = malloc(arg.length * 4, 4) >>> 0;
101 | getFloat32ArrayMemory0().set(arg, ptr / 4);
102 | WASM_VECTOR_LEN = arg.length;
103 | return ptr;
104 | }
105 |
106 | function getArrayF32FromWasm0(ptr, len) {
107 | ptr = ptr >>> 0;
108 | return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
109 | }
110 |
111 | const SIMDMathFinalization = (typeof FinalizationRegistry === 'undefined')
112 | ? { register: () => {}, unregister: () => {} }
113 | : new FinalizationRegistry(ptr => wasm.__wbg_simdmath_free(ptr >>> 0, 1));
114 |
115 | export class SIMDMath {
116 |
117 | __destroy_into_raw() {
118 | const ptr = this.__wbg_ptr;
119 | this.__wbg_ptr = 0;
120 | SIMDMathFinalization.unregister(this);
121 | return ptr;
122 | }
123 |
124 | free() {
125 | const ptr = this.__destroy_into_raw();
126 | wasm.__wbg_simdmath_free(ptr, 0);
127 | }
128 | constructor() {
129 | const ret = wasm.simdmath_new();
130 | this.__wbg_ptr = ret >>> 0;
131 | SIMDMathFinalization.register(this, this.__wbg_ptr, this);
132 | return this;
133 | }
134 | /**
135 | * @param {Float32Array} vec_a
136 | * @param {Float32Array} vec_b
137 | * @returns {number}
138 | */
139 | cosine_similarity(vec_a, vec_b) {
140 | const ptr0 = passArrayF32ToWasm0(vec_a, wasm.__wbindgen_malloc);
141 | const len0 = WASM_VECTOR_LEN;
142 | const ptr1 = passArrayF32ToWasm0(vec_b, wasm.__wbindgen_malloc);
143 | const len1 = WASM_VECTOR_LEN;
144 | const ret = wasm.simdmath_cosine_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1);
145 | return ret;
146 | }
147 | /**
148 | * @param {Float32Array} vectors
149 | * @param {Float32Array} query
150 | * @param {number} vector_dim
151 | * @returns {Float32Array}
152 | */
153 | batch_similarity(vectors, query, vector_dim) {
154 | const ptr0 = passArrayF32ToWasm0(vectors, wasm.__wbindgen_malloc);
155 | const len0 = WASM_VECTOR_LEN;
156 | const ptr1 = passArrayF32ToWasm0(query, wasm.__wbindgen_malloc);
157 | const len1 = WASM_VECTOR_LEN;
158 | const ret = wasm.simdmath_batch_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
159 | var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
160 | wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
161 | return v3;
162 | }
163 | /**
164 | * @param {Float32Array} vectors_a
165 | * @param {Float32Array} vectors_b
166 | * @param {number} vector_dim
167 | * @returns {Float32Array}
168 | */
169 | similarity_matrix(vectors_a, vectors_b, vector_dim) {
170 | const ptr0 = passArrayF32ToWasm0(vectors_a, wasm.__wbindgen_malloc);
171 | const len0 = WASM_VECTOR_LEN;
172 | const ptr1 = passArrayF32ToWasm0(vectors_b, wasm.__wbindgen_malloc);
173 | const len1 = WASM_VECTOR_LEN;
174 | const ret = wasm.simdmath_similarity_matrix(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
175 | var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
176 | wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
177 | return v3;
178 | }
179 | }
180 |
181 | async function __wbg_load(module, imports) {
182 | if (typeof Response === 'function' && module instanceof Response) {
183 | if (typeof WebAssembly.instantiateStreaming === 'function') {
184 | try {
185 | return await WebAssembly.instantiateStreaming(module, imports);
186 |
187 | } catch (e) {
188 | if (module.headers.get('Content-Type') != 'application/wasm') {
189 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
190 |
191 | } else {
192 | throw e;
193 | }
194 | }
195 | }
196 |
197 | const bytes = await module.arrayBuffer();
198 | return await WebAssembly.instantiate(bytes, imports);
199 |
200 | } else {
201 | const instance = await WebAssembly.instantiate(module, imports);
202 |
203 | if (instance instanceof WebAssembly.Instance) {
204 | return { instance, module };
205 |
206 | } else {
207 | return instance;
208 | }
209 | }
210 | }
211 |
212 | function __wbg_get_imports() {
213 | const imports = {};
214 | imports.wbg = {};
215 | imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {
216 | let deferred0_0;
217 | let deferred0_1;
218 | try {
219 | deferred0_0 = arg0;
220 | deferred0_1 = arg1;
221 | console.error(getStringFromWasm0(arg0, arg1));
222 | } finally {
223 | wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
224 | }
225 | };
226 | imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {
227 | const ret = new Error();
228 | return ret;
229 | };
230 | imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {
231 | const ret = arg1.stack;
232 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
233 | const len1 = WASM_VECTOR_LEN;
234 | getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
235 | getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
236 | };
237 | imports.wbg.__wbindgen_init_externref_table = function() {
238 | const table = wasm.__wbindgen_export_3;
239 | const offset = table.grow(4);
240 | table.set(0, undefined);
241 | table.set(offset + 0, undefined);
242 | table.set(offset + 1, null);
243 | table.set(offset + 2, true);
244 | table.set(offset + 3, false);
245 | ;
246 | };
247 | imports.wbg.__wbindgen_throw = function(arg0, arg1) {
248 | throw new Error(getStringFromWasm0(arg0, arg1));
249 | };
250 |
251 | return imports;
252 | }
253 |
254 | function __wbg_init_memory(imports, memory) {
255 |
256 | }
257 |
258 | function __wbg_finalize_init(instance, module) {
259 | wasm = instance.exports;
260 | __wbg_init.__wbindgen_wasm_module = module;
261 | cachedDataViewMemory0 = null;
262 | cachedFloat32ArrayMemory0 = null;
263 | cachedUint8ArrayMemory0 = null;
264 |
265 |
266 | wasm.__wbindgen_start();
267 | return wasm;
268 | }
269 |
270 | function initSync(module) {
271 | if (wasm !== undefined) return wasm;
272 |
273 |
274 | if (typeof module !== 'undefined') {
275 | if (Object.getPrototypeOf(module) === Object.prototype) {
276 | ({module} = module)
277 | } else {
278 | console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
279 | }
280 | }
281 |
282 | const imports = __wbg_get_imports();
283 |
284 | __wbg_init_memory(imports);
285 |
286 | if (!(module instanceof WebAssembly.Module)) {
287 | module = new WebAssembly.Module(module);
288 | }
289 |
290 | const instance = new WebAssembly.Instance(module, imports);
291 |
292 | return __wbg_finalize_init(instance, module);
293 | }
294 |
295 | async function __wbg_init(module_or_path) {
296 | if (wasm !== undefined) return wasm;
297 |
298 |
299 | if (typeof module_or_path !== 'undefined') {
300 | if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
301 | ({module_or_path} = module_or_path)
302 | } else {
303 | console.warn('using deprecated parameters for the initialization function; pass a single object instead')
304 | }
305 | }
306 |
307 | if (typeof module_or_path === 'undefined') {
308 | module_or_path = new URL('simd_math_bg.wasm', import.meta.url);
309 | }
310 | const imports = __wbg_get_imports();
311 |
312 | if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
313 | module_or_path = fetch(module_or_path);
314 | }
315 |
316 | __wbg_init_memory(imports);
317 |
318 | const { instance, module } = await __wbg_load(await module_or_path, imports);
319 |
320 | return __wbg_finalize_init(instance, module);
321 | }
322 |
323 | export { initSync };
324 | export default __wbg_init;
325 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/console.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { BaseBrowserToolExecutor } from '../base-browser';
3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
4 |
5 | const DEBUGGER_PROTOCOL_VERSION = '1.3';
6 | const DEFAULT_MAX_MESSAGES = 100;
7 |
8 | interface ConsoleToolParams {
9 | url?: string;
10 | includeExceptions?: boolean;
11 | maxMessages?: number;
12 | }
13 |
14 | interface ConsoleMessage {
15 | timestamp: number;
16 | level: string;
17 | text: string;
18 | args?: any[];
19 | source?: string;
20 | url?: string;
21 | lineNumber?: number;
22 | stackTrace?: any;
23 | }
24 |
25 | interface ConsoleException {
26 | timestamp: number;
27 | text: string;
28 | url?: string;
29 | lineNumber?: number;
30 | columnNumber?: number;
31 | stackTrace?: any;
32 | }
33 |
34 | interface ConsoleResult {
35 | success: boolean;
36 | message: string;
37 | tabId: number;
38 | tabUrl: string;
39 | tabTitle: string;
40 | captureStartTime: number;
41 | captureEndTime: number;
42 | totalDurationMs: number;
43 | messages: ConsoleMessage[];
44 | exceptions: ConsoleException[];
45 | messageCount: number;
46 | exceptionCount: number;
47 | messageLimitReached: boolean;
48 | }
49 |
50 | /**
51 | * Tool for capturing console output from browser tabs
52 | */
53 | class ConsoleTool extends BaseBrowserToolExecutor {
54 | name = TOOL_NAMES.BROWSER.CONSOLE;
55 |
56 | async execute(args: ConsoleToolParams): Promise<ToolResult> {
57 | const { url, includeExceptions = true, maxMessages = DEFAULT_MAX_MESSAGES } = args;
58 |
59 | let targetTab: chrome.tabs.Tab;
60 |
61 | try {
62 | if (url) {
63 | // Navigate to the specified URL
64 | targetTab = await this.navigateToUrl(url);
65 | } else {
66 | // Use current active tab
67 | const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
68 | if (!activeTab?.id) {
69 | return createErrorResponse('No active tab found and no URL provided.');
70 | }
71 | targetTab = activeTab;
72 | }
73 |
74 | if (!targetTab?.id) {
75 | return createErrorResponse('Failed to identify target tab.');
76 | }
77 |
78 | const tabId = targetTab.id;
79 |
80 | // Capture console messages (one-time capture)
81 | const result = await this.captureConsoleMessages(tabId, {
82 | includeExceptions,
83 | maxMessages,
84 | });
85 |
86 | return {
87 | content: [
88 | {
89 | type: 'text',
90 | text: JSON.stringify(result),
91 | },
92 | ],
93 | isError: false,
94 | };
95 | } catch (error: any) {
96 | console.error('ConsoleTool: Critical error during execute:', error);
97 | return createErrorResponse(`Error in ConsoleTool: ${error.message || String(error)}`);
98 | }
99 | }
100 |
101 | private async navigateToUrl(url: string): Promise<chrome.tabs.Tab> {
102 | // Check if URL is already open
103 | const existingTabs = await chrome.tabs.query({ url });
104 |
105 | if (existingTabs.length > 0 && existingTabs[0]?.id) {
106 | const tab = existingTabs[0];
107 | // Activate the existing tab
108 | await chrome.tabs.update(tab.id!, { active: true });
109 | await chrome.windows.update(tab.windowId, { focused: true });
110 | return tab;
111 | } else {
112 | // Create new tab with the URL
113 | const newTab = await chrome.tabs.create({ url, active: true });
114 | // Wait for tab to be ready
115 | await this.waitForTabReady(newTab.id!);
116 | return newTab;
117 | }
118 | }
119 |
120 | private async waitForTabReady(tabId: number): Promise<void> {
121 | return new Promise((resolve) => {
122 | const checkTab = async () => {
123 | try {
124 | const tab = await chrome.tabs.get(tabId);
125 | if (tab.status === 'complete') {
126 | resolve();
127 | } else {
128 | setTimeout(checkTab, 100);
129 | }
130 | } catch (error) {
131 | // Tab might be closed, resolve anyway
132 | resolve();
133 | }
134 | };
135 | checkTab();
136 | });
137 | }
138 |
139 | private formatConsoleArgs(args: any[]): string {
140 | if (!args || args.length === 0) return '';
141 |
142 | return args
143 | .map((arg) => {
144 | if (arg.type === 'string') {
145 | return arg.value || '';
146 | } else if (arg.type === 'number') {
147 | return String(arg.value || '');
148 | } else if (arg.type === 'boolean') {
149 | return String(arg.value || '');
150 | } else if (arg.type === 'object') {
151 | return arg.description || '[Object]';
152 | } else if (arg.type === 'undefined') {
153 | return 'undefined';
154 | } else if (arg.type === 'function') {
155 | return arg.description || '[Function]';
156 | } else {
157 | return arg.description || arg.value || String(arg);
158 | }
159 | })
160 | .join(' ');
161 | }
162 |
163 | private async captureConsoleMessages(
164 | tabId: number,
165 | options: {
166 | includeExceptions: boolean;
167 | maxMessages: number;
168 | },
169 | ): Promise<ConsoleResult> {
170 | const { includeExceptions, maxMessages } = options;
171 | const startTime = Date.now();
172 | const messages: ConsoleMessage[] = [];
173 | const exceptions: ConsoleException[] = [];
174 | let limitReached = false;
175 |
176 | try {
177 | // Get tab information
178 | const tab = await chrome.tabs.get(tabId);
179 |
180 | // Check if debugger is already attached
181 | const targets = await chrome.debugger.getTargets();
182 | const existingTarget = targets.find(
183 | (t) => t.tabId === tabId && t.attached && t.type === 'page',
184 | );
185 | if (existingTarget && !existingTarget.extensionId) {
186 | throw new Error(
187 | `Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
188 | );
189 | }
190 |
191 | // Attach debugger
192 | try {
193 | await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
194 | } catch (error: any) {
195 | if (error.message?.includes('Cannot attach to the target with an attached client')) {
196 | throw new Error(
197 | `Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
198 | );
199 | }
200 | throw error;
201 | }
202 |
203 | // Set up event listener to collect messages
204 | const collectedMessages: any[] = [];
205 | const collectedExceptions: any[] = [];
206 |
207 | const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
208 | if (source.tabId !== tabId) return;
209 |
210 | if (method === 'Log.entryAdded' && params?.entry) {
211 | collectedMessages.push(params.entry);
212 | } else if (method === 'Runtime.consoleAPICalled' && params) {
213 | // Convert Runtime.consoleAPICalled to Log.entryAdded format
214 | const logEntry = {
215 | timestamp: params.timestamp,
216 | level: params.type || 'log',
217 | text: this.formatConsoleArgs(params.args || []),
218 | source: 'console-api',
219 | url: params.stackTrace?.callFrames?.[0]?.url,
220 | lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
221 | stackTrace: params.stackTrace,
222 | args: params.args,
223 | };
224 | collectedMessages.push(logEntry);
225 | } else if (
226 | method === 'Runtime.exceptionThrown' &&
227 | includeExceptions &&
228 | params?.exceptionDetails
229 | ) {
230 | collectedExceptions.push(params.exceptionDetails);
231 | }
232 | };
233 |
234 | chrome.debugger.onEvent.addListener(eventListener);
235 |
236 | try {
237 | // Enable Runtime domain first to capture console API calls and exceptions
238 | await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
239 |
240 | // Also enable Log domain to capture other log entries
241 | await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
242 |
243 | // Wait for all messages to be flushed
244 | await new Promise((resolve) => setTimeout(resolve, 2000));
245 |
246 | // Process collected messages
247 | for (const entry of collectedMessages) {
248 | if (messages.length >= maxMessages) {
249 | limitReached = true;
250 | break;
251 | }
252 |
253 | const message: ConsoleMessage = {
254 | timestamp: entry.timestamp,
255 | level: entry.level || 'log',
256 | text: entry.text || '',
257 | source: entry.source,
258 | url: entry.url,
259 | lineNumber: entry.lineNumber,
260 | };
261 |
262 | if (entry.stackTrace) {
263 | message.stackTrace = entry.stackTrace;
264 | }
265 |
266 | if (entry.args && Array.isArray(entry.args)) {
267 | message.args = entry.args;
268 | }
269 |
270 | messages.push(message);
271 | }
272 |
273 | // Process collected exceptions
274 | for (const exceptionDetails of collectedExceptions) {
275 | const exception: ConsoleException = {
276 | timestamp: Date.now(),
277 | text:
278 | exceptionDetails.text ||
279 | exceptionDetails.exception?.description ||
280 | 'Unknown exception',
281 | url: exceptionDetails.url,
282 | lineNumber: exceptionDetails.lineNumber,
283 | columnNumber: exceptionDetails.columnNumber,
284 | };
285 |
286 | if (exceptionDetails.stackTrace) {
287 | exception.stackTrace = exceptionDetails.stackTrace;
288 | }
289 |
290 | exceptions.push(exception);
291 | }
292 | } finally {
293 | // Clean up
294 | chrome.debugger.onEvent.removeListener(eventListener);
295 |
296 | try {
297 | await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable');
298 | } catch (e) {
299 | console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
300 | }
301 |
302 | try {
303 | await chrome.debugger.sendCommand({ tabId }, 'Log.disable');
304 | } catch (e) {
305 | console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
306 | }
307 |
308 | try {
309 | await chrome.debugger.detach({ tabId });
310 | } catch (e) {
311 | console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
312 | }
313 | }
314 |
315 | const endTime = Date.now();
316 |
317 | // Sort messages by timestamp
318 | messages.sort((a, b) => a.timestamp - b.timestamp);
319 | exceptions.sort((a, b) => a.timestamp - b.timestamp);
320 |
321 | return {
322 | success: true,
323 | message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
324 | tabId,
325 | tabUrl: tab.url || '',
326 | tabTitle: tab.title || '',
327 | captureStartTime: startTime,
328 | captureEndTime: endTime,
329 | totalDurationMs: endTime - startTime,
330 | messages,
331 | exceptions,
332 | messageCount: messages.length,
333 | exceptionCount: exceptions.length,
334 | messageLimitReached: limitReached,
335 | };
336 | } catch (error: any) {
337 | console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
338 | throw error;
339 | }
340 | }
341 | }
342 |
343 | export const consoleTool = new ConsoleTool();
344 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/model-cache-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Model Cache Manager
3 | */
4 |
5 | const CACHE_NAME = 'onnx-model-cache-v1';
6 | const CACHE_EXPIRY_DAYS = 30;
7 | const MAX_CACHE_SIZE_MB = 500;
8 |
9 | export interface CacheMetadata {
10 | timestamp: number;
11 | modelUrl: string;
12 | size: number;
13 | version: string;
14 | }
15 |
16 | export interface CacheEntry {
17 | url: string;
18 | size: number;
19 | sizeMB: number;
20 | timestamp: number;
21 | age: string;
22 | expired: boolean;
23 | }
24 |
25 | export interface CacheStats {
26 | totalSize: number;
27 | totalSizeMB: number;
28 | entryCount: number;
29 | entries: CacheEntry[];
30 | }
31 |
32 | interface CacheEntryDetails {
33 | url: string;
34 | timestamp: number;
35 | size: number;
36 | }
37 |
38 | export class ModelCacheManager {
39 | private static instance: ModelCacheManager | null = null;
40 |
41 | public static getInstance(): ModelCacheManager {
42 | if (!ModelCacheManager.instance) {
43 | ModelCacheManager.instance = new ModelCacheManager();
44 | }
45 | return ModelCacheManager.instance;
46 | }
47 |
48 | private constructor() {}
49 |
50 | private getCacheMetadataKey(modelUrl: string): string {
51 | const encodedUrl = encodeURIComponent(modelUrl);
52 | return `https://cache-metadata.local/${encodedUrl}`;
53 | }
54 |
55 | private isCacheExpired(metadata: CacheMetadata): boolean {
56 | const now = Date.now();
57 | const expiryTime = metadata.timestamp + CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
58 | return now > expiryTime;
59 | }
60 |
61 | private isMetadataUrl(url: string): boolean {
62 | return url.startsWith('https://cache-metadata.local/');
63 | }
64 |
65 | private async collectCacheEntries(): Promise<{
66 | entries: CacheEntryDetails[];
67 | totalSize: number;
68 | entryCount: number;
69 | }> {
70 | const cache = await caches.open(CACHE_NAME);
71 | const keys = await cache.keys();
72 | const entries: CacheEntryDetails[] = [];
73 | let totalSize = 0;
74 | let entryCount = 0;
75 |
76 | for (const request of keys) {
77 | if (this.isMetadataUrl(request.url)) continue;
78 |
79 | const response = await cache.match(request);
80 | if (response) {
81 | const blob = await response.blob();
82 | const size = blob.size;
83 | totalSize += size;
84 | entryCount++;
85 |
86 | const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
87 | let timestamp = 0;
88 |
89 | if (metadataResponse) {
90 | try {
91 | const metadata: CacheMetadata = await metadataResponse.json();
92 | timestamp = metadata.timestamp;
93 | } catch (error) {
94 | console.warn('Failed to parse cache metadata:', error);
95 | }
96 | }
97 |
98 | entries.push({
99 | url: request.url,
100 | timestamp,
101 | size,
102 | });
103 | }
104 | }
105 |
106 | return { entries, totalSize, entryCount };
107 | }
108 |
109 | public async cleanupCacheOnDemand(newDataSize: number = 0): Promise<void> {
110 | const cache = await caches.open(CACHE_NAME);
111 | const { entries, totalSize } = await this.collectCacheEntries();
112 | const maxSizeBytes = MAX_CACHE_SIZE_MB * 1024 * 1024;
113 | const projectedSize = totalSize + newDataSize;
114 |
115 | if (projectedSize <= maxSizeBytes) {
116 | return;
117 | }
118 |
119 | console.log(
120 | `Cache size (${(totalSize / 1024 / 1024).toFixed(2)}MB) + new data (${(newDataSize / 1024 / 1024).toFixed(2)}MB) exceeds limit (${MAX_CACHE_SIZE_MB}MB), cleaning up...`,
121 | );
122 |
123 | const expiredEntries: CacheEntryDetails[] = [];
124 | const validEntries: CacheEntryDetails[] = [];
125 |
126 | for (const entry of entries) {
127 | const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
128 | let isExpired = false;
129 |
130 | if (metadataResponse) {
131 | try {
132 | const metadata: CacheMetadata = await metadataResponse.json();
133 | isExpired = this.isCacheExpired(metadata);
134 | } catch (error) {
135 | isExpired = true;
136 | }
137 | } else {
138 | isExpired = true;
139 | }
140 |
141 | if (isExpired) {
142 | expiredEntries.push(entry);
143 | } else {
144 | validEntries.push(entry);
145 | }
146 | }
147 |
148 | let currentSize = totalSize;
149 | for (const entry of expiredEntries) {
150 | await cache.delete(entry.url);
151 | await cache.delete(this.getCacheMetadataKey(entry.url));
152 | currentSize -= entry.size;
153 | console.log(
154 | `Cleaned up expired cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
155 | );
156 | }
157 |
158 | if (currentSize + newDataSize > maxSizeBytes) {
159 | validEntries.sort((a, b) => a.timestamp - b.timestamp);
160 |
161 | for (const entry of validEntries) {
162 | if (currentSize + newDataSize <= maxSizeBytes) break;
163 |
164 | await cache.delete(entry.url);
165 | await cache.delete(this.getCacheMetadataKey(entry.url));
166 | currentSize -= entry.size;
167 | console.log(
168 | `Cleaned up old cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
169 | );
170 | }
171 | }
172 |
173 | console.log(`Cache cleanup complete. New size: ${(currentSize / 1024 / 1024).toFixed(2)}MB`);
174 | }
175 |
176 | public async storeCacheMetadata(modelUrl: string, size: number): Promise<void> {
177 | const cache = await caches.open(CACHE_NAME);
178 | const metadata: CacheMetadata = {
179 | timestamp: Date.now(),
180 | modelUrl,
181 | size,
182 | version: CACHE_NAME,
183 | };
184 |
185 | const metadataResponse = new Response(JSON.stringify(metadata), {
186 | headers: { 'Content-Type': 'application/json' },
187 | });
188 |
189 | await cache.put(this.getCacheMetadataKey(modelUrl), metadataResponse);
190 | }
191 |
192 | public async getCachedModelData(modelUrl: string): Promise<ArrayBuffer | null> {
193 | const cache = await caches.open(CACHE_NAME);
194 | const cachedResponse = await cache.match(modelUrl);
195 |
196 | if (!cachedResponse) {
197 | return null;
198 | }
199 |
200 | const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
201 | if (metadataResponse) {
202 | try {
203 | const metadata: CacheMetadata = await metadataResponse.json();
204 | if (!this.isCacheExpired(metadata)) {
205 | console.log('Model found in cache and not expired. Loading from cache.');
206 | return cachedResponse.arrayBuffer();
207 | } else {
208 | console.log('Cached model is expired, removing...');
209 | await this.deleteCacheEntry(modelUrl);
210 | return null;
211 | }
212 | } catch (error) {
213 | console.warn('Failed to parse cache metadata, treating as expired:', error);
214 | await this.deleteCacheEntry(modelUrl);
215 | return null;
216 | }
217 | } else {
218 | console.log('Cached model has no metadata, treating as expired...');
219 | await this.deleteCacheEntry(modelUrl);
220 | return null;
221 | }
222 | }
223 |
224 | public async storeModelData(modelUrl: string, data: ArrayBuffer): Promise<void> {
225 | await this.cleanupCacheOnDemand(data.byteLength);
226 |
227 | const cache = await caches.open(CACHE_NAME);
228 | const response = new Response(data);
229 |
230 | await cache.put(modelUrl, response);
231 | await this.storeCacheMetadata(modelUrl, data.byteLength);
232 |
233 | console.log(
234 | `Model cached successfully (${(data.byteLength / 1024 / 1024).toFixed(2)}MB): ${modelUrl}`,
235 | );
236 | }
237 |
238 | public async deleteCacheEntry(modelUrl: string): Promise<void> {
239 | const cache = await caches.open(CACHE_NAME);
240 | await cache.delete(modelUrl);
241 | await cache.delete(this.getCacheMetadataKey(modelUrl));
242 | }
243 |
244 | public async clearAllCache(): Promise<void> {
245 | const cache = await caches.open(CACHE_NAME);
246 | const keys = await cache.keys();
247 |
248 | for (const request of keys) {
249 | await cache.delete(request);
250 | }
251 |
252 | console.log('All model cache entries cleared');
253 | }
254 |
255 | public async getCacheStats(): Promise<CacheStats> {
256 | const { entries, totalSize, entryCount } = await this.collectCacheEntries();
257 | const cache = await caches.open(CACHE_NAME);
258 |
259 | const cacheEntries: CacheEntry[] = [];
260 |
261 | for (const entry of entries) {
262 | const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
263 | let expired = false;
264 |
265 | if (metadataResponse) {
266 | try {
267 | const metadata: CacheMetadata = await metadataResponse.json();
268 | expired = this.isCacheExpired(metadata);
269 | } catch (error) {
270 | expired = true;
271 | }
272 | } else {
273 | expired = true;
274 | }
275 |
276 | const age =
277 | entry.timestamp > 0
278 | ? `${Math.round((Date.now() - entry.timestamp) / (1000 * 60 * 60 * 24))} days`
279 | : 'unknown';
280 |
281 | cacheEntries.push({
282 | url: entry.url,
283 | size: entry.size,
284 | sizeMB: Number((entry.size / 1024 / 1024).toFixed(2)),
285 | timestamp: entry.timestamp,
286 | age,
287 | expired,
288 | });
289 | }
290 |
291 | return {
292 | totalSize,
293 | totalSizeMB: Number((totalSize / 1024 / 1024).toFixed(2)),
294 | entryCount,
295 | entries: cacheEntries.sort((a, b) => b.timestamp - a.timestamp),
296 | };
297 | }
298 |
299 | public async manualCleanup(): Promise<void> {
300 | await this.cleanupCacheOnDemand(0);
301 | console.log('Manual cache cleanup completed');
302 | }
303 |
304 | /**
305 | * Check if a specific model is cached and not expired
306 | * @param modelUrl The model URL to check
307 | * @returns Promise<boolean> True if model is cached and valid
308 | */
309 | public async isModelCached(modelUrl: string): Promise<boolean> {
310 | try {
311 | const cache = await caches.open(CACHE_NAME);
312 | const cachedResponse = await cache.match(modelUrl);
313 |
314 | if (!cachedResponse) {
315 | return false;
316 | }
317 |
318 | const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
319 | if (metadataResponse) {
320 | try {
321 | const metadata: CacheMetadata = await metadataResponse.json();
322 | return !this.isCacheExpired(metadata);
323 | } catch (error) {
324 | console.warn('Failed to parse cache metadata for cache check:', error);
325 | return false;
326 | }
327 | } else {
328 | // No metadata means expired
329 | return false;
330 | }
331 | } catch (error) {
332 | console.error('Error checking model cache:', error);
333 | return false;
334 | }
335 | }
336 |
337 | /**
338 | * Check if any valid (non-expired) model cache exists
339 | * @returns Promise<boolean> True if at least one valid model cache exists
340 | */
341 | public async hasAnyValidCache(): Promise<boolean> {
342 | try {
343 | const cache = await caches.open(CACHE_NAME);
344 | const keys = await cache.keys();
345 |
346 | for (const request of keys) {
347 | if (this.isMetadataUrl(request.url)) continue;
348 |
349 | const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
350 | if (metadataResponse) {
351 | try {
352 | const metadata: CacheMetadata = await metadataResponse.json();
353 | if (!this.isCacheExpired(metadata)) {
354 | return true; // Found at least one valid cache
355 | }
356 | } catch (error) {
357 | // Skip invalid metadata
358 | continue;
359 | }
360 | }
361 | }
362 |
363 | return false;
364 | } catch (error) {
365 | console.error('Error checking for valid cache:', error);
366 | return false;
367 | }
368 | }
369 | }
370 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/keyboard-helper.js:
--------------------------------------------------------------------------------
```javascript
1 | /* eslint-disable */
2 | // keyboard-helper.js
3 | // This script is injected into the page to handle keyboard event simulation
4 |
5 | if (window.__KEYBOARD_HELPER_INITIALIZED__) {
6 | // Already initialized, skip
7 | } else {
8 | window.__KEYBOARD_HELPER_INITIALIZED__ = true;
9 |
10 | // A map for special keys to their KeyboardEvent properties
11 | // Key names should be lowercase for matching
12 | const SPECIAL_KEY_MAP = {
13 | enter: { key: 'Enter', code: 'Enter', keyCode: 13 },
14 | tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
15 | esc: { key: 'Escape', code: 'Escape', keyCode: 27 },
16 | escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
17 | space: { key: ' ', code: 'Space', keyCode: 32 },
18 | backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
19 | delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
20 | del: { key: 'Delete', code: 'Delete', keyCode: 46 },
21 | up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
22 | arrowup: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
23 | down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
24 | arrowdown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
25 | left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
26 | arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
27 | right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
28 | arrowright: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
29 | home: { key: 'Home', code: 'Home', keyCode: 36 },
30 | end: { key: 'End', code: 'End', keyCode: 35 },
31 | pageup: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
32 | pagedown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
33 | insert: { key: 'Insert', code: 'Insert', keyCode: 45 },
34 | // Function keys
35 | ...Object.fromEntries(
36 | Array.from({ length: 12 }, (_, i) => [
37 | `f${i + 1}`,
38 | { key: `F${i + 1}`, code: `F${i + 1}`, keyCode: 112 + i },
39 | ]),
40 | ),
41 | };
42 |
43 | const MODIFIER_KEYS = {
44 | ctrl: 'ctrlKey',
45 | control: 'ctrlKey',
46 | alt: 'altKey',
47 | shift: 'shiftKey',
48 | meta: 'metaKey',
49 | command: 'metaKey',
50 | cmd: 'metaKey',
51 | };
52 |
53 | /**
54 | * Parses a key string (e.g., "Ctrl+Shift+A", "Enter") into a main key and modifiers.
55 | * @param {string} keyString - String representation of a single key press (can include modifiers).
56 | * @returns { {key: string, code: string, keyCode: number, charCode?: number, modifiers: {ctrlKey:boolean, altKey:boolean, shiftKey:boolean, metaKey:boolean}} | null }
57 | * Returns null if the keyString is invalid or represents only modifiers.
58 | */
59 | function parseSingleKeyCombination(keyString) {
60 | const parts = keyString.split('+').map((part) => part.trim().toLowerCase());
61 | const modifiers = {
62 | ctrlKey: false,
63 | altKey: false,
64 | shiftKey: false,
65 | metaKey: false,
66 | };
67 | let mainKeyPart = null;
68 |
69 | for (const part of parts) {
70 | if (MODIFIER_KEYS[part]) {
71 | modifiers[MODIFIER_KEYS[part]] = true;
72 | } else if (mainKeyPart === null) {
73 | // First non-modifier is the main key
74 | mainKeyPart = part;
75 | } else {
76 | // Invalid format: multiple main keys in a single combination (e.g., "Ctrl+A+B")
77 | console.error(`Invalid key combination string: ${keyString}. Multiple main keys found.`);
78 | return null;
79 | }
80 | }
81 |
82 | if (!mainKeyPart) {
83 | // This case could happen if the keyString is something like "Ctrl+" or just "Ctrl"
84 | // If the intent was to press JUST 'Control', the input should be 'Control' not 'Control+'
85 | // Let's check if mainKeyPart is actually a modifier name used as a main key
86 | if (Object.keys(MODIFIER_KEYS).includes(parts[parts.length - 1]) && parts.length === 1) {
87 | mainKeyPart = parts[parts.length - 1]; // e.g. user wants to press "Control" key itself
88 | // For "Control" key itself, key: "Control", code: "ControlLeft" (or Right)
89 | if (mainKeyPart === 'ctrl' || mainKeyPart === 'control')
90 | return { key: 'Control', code: 'ControlLeft', keyCode: 17, modifiers };
91 | if (mainKeyPart === 'alt') return { key: 'Alt', code: 'AltLeft', keyCode: 18, modifiers };
92 | if (mainKeyPart === 'shift')
93 | return { key: 'Shift', code: 'ShiftLeft', keyCode: 16, modifiers };
94 | if (mainKeyPart === 'meta' || mainKeyPart === 'command' || mainKeyPart === 'cmd')
95 | return { key: 'Meta', code: 'MetaLeft', keyCode: 91, modifiers };
96 | } else {
97 | console.error(`Invalid key combination string: ${keyString}. No main key specified.`);
98 | return null;
99 | }
100 | }
101 |
102 | const specialKey = SPECIAL_KEY_MAP[mainKeyPart];
103 | if (specialKey) {
104 | return { ...specialKey, modifiers };
105 | }
106 |
107 | // For single characters or other unmapped keys
108 | if (mainKeyPart.length === 1) {
109 | const charCode = mainKeyPart.charCodeAt(0);
110 | // If Shift is active and it's a letter, use the uppercase version for 'key'
111 | // This mimics more closely how keyboards behave.
112 | let keyChar = mainKeyPart;
113 | if (modifiers.shiftKey && mainKeyPart.match(/^[a-z]$/i)) {
114 | keyChar = mainKeyPart.toUpperCase();
115 | }
116 |
117 | return {
118 | key: keyChar,
119 | code: `Key${mainKeyPart.toUpperCase()}`, // 'a' -> KeyA, 'A' -> KeyA
120 | keyCode: charCode,
121 | charCode: charCode, // charCode is legacy, but some old systems might use it
122 | modifiers,
123 | };
124 | }
125 |
126 | console.error(`Unknown key: ${mainKeyPart} in string "${keyString}"`);
127 | return null; // Or handle as an error
128 | }
129 |
130 | /**
131 | * Simulates a single key press (keydown, (keypress), keyup) for a parsed key.
132 | * @param { {key: string, code: string, keyCode: number, charCode?: number, modifiers: object} } parsedKeyInfo
133 | * @param {Element} element - Target element.
134 | * @returns {{success: boolean, error?: string}}
135 | */
136 | function dispatchKeyEvents(parsedKeyInfo, element) {
137 | if (!parsedKeyInfo) return { success: false, error: 'Invalid key info provided for dispatch.' };
138 |
139 | const { key, code, keyCode, charCode, modifiers } = parsedKeyInfo;
140 |
141 | const eventOptions = {
142 | key: key,
143 | code: code,
144 | bubbles: true,
145 | cancelable: true,
146 | composed: true, // Important for shadow DOM
147 | view: window,
148 | ...modifiers, // ctrlKey, altKey, shiftKey, metaKey
149 | // keyCode/which are deprecated but often set for compatibility
150 | keyCode: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
151 | which: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
152 | };
153 |
154 | try {
155 | const kdRes = element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
156 |
157 | // keypress is deprecated, but simulate if it's a character key or Enter
158 | // Only dispatch if keydown was not cancelled and it's a character producing key
159 | if (kdRes && (key.length === 1 || key === 'Enter' || key === ' ')) {
160 | const keypressOptions = { ...eventOptions };
161 | if (charCode) keypressOptions.charCode = charCode;
162 | element.dispatchEvent(new KeyboardEvent('keypress', keypressOptions));
163 | }
164 |
165 | element.dispatchEvent(new KeyboardEvent('keyup', eventOptions));
166 | return { success: true };
167 | } catch (error) {
168 | console.error(`Error dispatching key events for "${key}":`, error);
169 | return {
170 | success: false,
171 | error: `Error dispatching key events for "${key}": ${error.message}`,
172 | };
173 | }
174 | }
175 |
176 | /**
177 | * Simulate keyboard events on an element or document
178 | * @param {string} keysSequenceString - String representation of key(s) (e.g., "Enter", "Ctrl+C, A, B")
179 | * @param {Element} targetElement - Element to dispatch events on (optional)
180 | * @param {number} delay - Delay between key sequences in milliseconds (optional)
181 | * @returns {Promise<Object>} - Result of the keyboard operation
182 | */
183 | async function simulateKeyboard(keysSequenceString, targetElement = null, delay = 0) {
184 | try {
185 | const element = targetElement || document.activeElement || document.body;
186 |
187 | if (element !== document.activeElement && typeof element.focus === 'function') {
188 | element.focus();
189 | await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for focus
190 | }
191 |
192 | const keyCombinations = keysSequenceString
193 | .split(',')
194 | .map((k) => k.trim())
195 | .filter((k) => k.length > 0);
196 | const operationResults = [];
197 |
198 | for (let i = 0; i < keyCombinations.length; i++) {
199 | const comboString = keyCombinations[i];
200 | const parsedKeyInfo = parseSingleKeyCombination(comboString);
201 |
202 | if (!parsedKeyInfo) {
203 | operationResults.push({
204 | keyCombination: comboString,
205 | success: false,
206 | error: `Invalid key string or combination: ${comboString}`,
207 | });
208 | continue; // Skip to next combination in sequence
209 | }
210 |
211 | const dispatchResult = dispatchKeyEvents(parsedKeyInfo, element);
212 | operationResults.push({
213 | keyCombination: comboString,
214 | ...dispatchResult,
215 | });
216 |
217 | if (dispatchResult.error) {
218 | // Optionally, decide if sequence should stop on first error
219 | // For now, we continue but log the error in results
220 | console.warn(
221 | `Failed to simulate key combination "${comboString}": ${dispatchResult.error}`,
222 | );
223 | }
224 |
225 | if (delay > 0 && i < keyCombinations.length - 1) {
226 | await new Promise((resolve) => setTimeout(resolve, delay));
227 | }
228 | }
229 |
230 | // Check if all individual operations were successful
231 | const overallSuccess = operationResults.every((r) => r.success);
232 |
233 | return {
234 | success: overallSuccess,
235 | message: overallSuccess
236 | ? `Keyboard events simulated successfully: ${keysSequenceString}`
237 | : `Some keyboard events failed for: ${keysSequenceString}`,
238 | results: operationResults, // Detailed results for each key combination
239 | targetElement: {
240 | tagName: element.tagName,
241 | id: element.id,
242 | className: element.className,
243 | type: element.type, // if applicable e.g. for input
244 | },
245 | };
246 | } catch (error) {
247 | console.error('Error in simulateKeyboard:', error);
248 | return {
249 | success: false,
250 | error: `Error simulating keyboard events: ${error.message}`,
251 | results: [],
252 | };
253 | }
254 | }
255 |
256 | // Listener for messages from the extension
257 | chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
258 | if (request.action === 'simulateKeyboard') {
259 | let targetEl = null;
260 | if (request.selector) {
261 | targetEl = document.querySelector(request.selector);
262 | if (!targetEl) {
263 | sendResponse({
264 | success: false,
265 | error: `Element with selector "${request.selector}" not found`,
266 | results: [],
267 | });
268 | return true; // Keep channel open for async response
269 | }
270 | }
271 |
272 | simulateKeyboard(request.keys, targetEl, request.delay)
273 | .then(sendResponse)
274 | .catch((error) => {
275 | // This catch is for unexpected errors in simulateKeyboard promise chain itself
276 | console.error('Unexpected error in simulateKeyboard promise chain:', error);
277 | sendResponse({
278 | success: false,
279 | error: `Unexpected error during keyboard simulation: ${error.message}`,
280 | results: [],
281 | });
282 | });
283 | return true; // Indicates async response is expected
284 | } else if (request.action === 'chrome_keyboard_ping') {
285 | sendResponse({ status: 'pong', initialized: true }); // Respond that it's initialized
286 | return false; // Synchronous response
287 | }
288 | // Not our message, or no async response needed
289 | return false;
290 | });
291 | }
292 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/semantic-similarity.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { ModelPreset } from '@/utils/semantic-similarity-engine';
2 | import { OffscreenManager } from '@/utils/offscreen-manager';
3 | import { BACKGROUND_MESSAGE_TYPES, OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';
4 | import { STORAGE_KEYS, ERROR_MESSAGES } from '@/common/constants';
5 | import { hasAnyModelCache } from '@/utils/semantic-similarity-engine';
6 |
7 | /**
8 | * Model configuration state management interface
9 | */
10 | interface ModelConfig {
11 | modelPreset: ModelPreset;
12 | modelVersion: 'full' | 'quantized' | 'compressed';
13 | modelDimension: number;
14 | }
15 |
16 | let currentBackgroundModelConfig: ModelConfig | null = null;
17 |
18 | /**
19 | * Initialize semantic engine only if model cache exists
20 | * This is called during plugin startup to avoid downloading models unnecessarily
21 | */
22 | export async function initializeSemanticEngineIfCached(): Promise<boolean> {
23 | try {
24 | console.log('Background: Checking if semantic engine should be initialized from cache...');
25 |
26 | const hasCachedModel = await hasAnyModelCache();
27 | if (!hasCachedModel) {
28 | console.log('Background: No cached models found, skipping semantic engine initialization');
29 | return false;
30 | }
31 |
32 | console.log('Background: Found cached models, initializing semantic engine...');
33 | await initializeDefaultSemanticEngine();
34 | return true;
35 | } catch (error) {
36 | console.error('Background: Error during conditional semantic engine initialization:', error);
37 | return false;
38 | }
39 | }
40 |
41 | /**
42 | * Initialize default semantic engine model
43 | */
44 | export async function initializeDefaultSemanticEngine(): Promise<void> {
45 | try {
46 | console.log('Background: Initializing default semantic engine...');
47 |
48 | // Update status to initializing
49 | await updateModelStatus('initializing', 0);
50 |
51 | const result = await chrome.storage.local.get([STORAGE_KEYS.SEMANTIC_MODEL, 'selectedVersion']);
52 | const defaultModel =
53 | (result[STORAGE_KEYS.SEMANTIC_MODEL] as ModelPreset) || 'multilingual-e5-small';
54 | const defaultVersion =
55 | (result.selectedVersion as 'full' | 'quantized' | 'compressed') || 'quantized';
56 |
57 | const { PREDEFINED_MODELS } = await import('@/utils/semantic-similarity-engine');
58 | const modelInfo = PREDEFINED_MODELS[defaultModel];
59 |
60 | await OffscreenManager.getInstance().ensureOffscreenDocument();
61 |
62 | const response = await chrome.runtime.sendMessage({
63 | target: 'offscreen',
64 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
65 | config: {
66 | useLocalFiles: false,
67 | modelPreset: defaultModel,
68 | modelVersion: defaultVersion,
69 | modelDimension: modelInfo.dimension,
70 | forceOffscreen: true,
71 | },
72 | });
73 |
74 | if (response && response.success) {
75 | currentBackgroundModelConfig = {
76 | modelPreset: defaultModel,
77 | modelVersion: defaultVersion,
78 | modelDimension: modelInfo.dimension,
79 | };
80 | console.log('Semantic engine initialized successfully:', currentBackgroundModelConfig);
81 |
82 | // Update status to ready
83 | await updateModelStatus('ready', 100);
84 |
85 | // Also initialize ContentIndexer now that semantic engine is ready
86 | try {
87 | const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
88 | const contentIndexer = getGlobalContentIndexer();
89 | contentIndexer.startSemanticEngineInitialization();
90 | console.log('ContentIndexer initialization triggered after semantic engine initialization');
91 | } catch (indexerError) {
92 | console.warn(
93 | 'Failed to initialize ContentIndexer after semantic engine initialization:',
94 | indexerError,
95 | );
96 | }
97 | } else {
98 | const errorMessage = response?.error || ERROR_MESSAGES.TOOL_EXECUTION_FAILED;
99 | await updateModelStatus('error', 0, errorMessage, 'unknown');
100 | throw new Error(errorMessage);
101 | }
102 | } catch (error: any) {
103 | console.error('Background: Failed to initialize default semantic engine:', error);
104 | const errorMessage = error?.message || 'Unknown error during semantic engine initialization';
105 | await updateModelStatus('error', 0, errorMessage, 'unknown');
106 | // Don't throw error, let the extension continue running
107 | }
108 | }
109 |
110 | /**
111 | * Check if model switch is needed
112 | */
113 | function needsModelSwitch(
114 | modelPreset: ModelPreset,
115 | modelVersion: 'full' | 'quantized' | 'compressed',
116 | modelDimension?: number,
117 | ): boolean {
118 | if (!currentBackgroundModelConfig) {
119 | return true;
120 | }
121 |
122 | const keyFields = ['modelPreset', 'modelVersion', 'modelDimension'];
123 | for (const field of keyFields) {
124 | const newValue =
125 | field === 'modelPreset'
126 | ? modelPreset
127 | : field === 'modelVersion'
128 | ? modelVersion
129 | : modelDimension;
130 | if (newValue !== currentBackgroundModelConfig[field as keyof ModelConfig]) {
131 | return true;
132 | }
133 | }
134 |
135 | return false;
136 | }
137 |
138 | /**
139 | * Handle model switching
140 | */
141 | export async function handleModelSwitch(
142 | modelPreset: ModelPreset,
143 | modelVersion: 'full' | 'quantized' | 'compressed' = 'quantized',
144 | modelDimension?: number,
145 | previousDimension?: number,
146 | ): Promise<{ success: boolean; error?: string }> {
147 | try {
148 | const needsSwitch = needsModelSwitch(modelPreset, modelVersion, modelDimension);
149 | if (!needsSwitch) {
150 | await updateModelStatus('ready', 100);
151 | return { success: true };
152 | }
153 |
154 | await updateModelStatus('downloading', 0);
155 |
156 | try {
157 | await OffscreenManager.getInstance().ensureOffscreenDocument();
158 | } catch (offscreenError) {
159 | console.error('Background: Failed to create offscreen document:', offscreenError);
160 | const errorMessage = `Failed to create offscreen document: ${offscreenError}`;
161 | await updateModelStatus('error', 0, errorMessage, 'unknown');
162 | return { success: false, error: errorMessage };
163 | }
164 |
165 | const response = await chrome.runtime.sendMessage({
166 | target: 'offscreen',
167 | type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
168 | config: {
169 | useLocalFiles: false,
170 | modelPreset: modelPreset,
171 | modelVersion: modelVersion,
172 | modelDimension: modelDimension,
173 | forceOffscreen: true,
174 | },
175 | });
176 |
177 | if (response && response.success) {
178 | currentBackgroundModelConfig = {
179 | modelPreset: modelPreset,
180 | modelVersion: modelVersion,
181 | modelDimension: modelDimension!,
182 | };
183 |
184 | // Only reinitialize ContentIndexer when dimension changes
185 | try {
186 | if (modelDimension && previousDimension && modelDimension !== previousDimension) {
187 | const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
188 | const contentIndexer = getGlobalContentIndexer();
189 | await contentIndexer.reinitialize();
190 | }
191 | } catch (indexerError) {
192 | console.warn('Background: Failed to reinitialize ContentIndexer:', indexerError);
193 | }
194 |
195 | await updateModelStatus('ready', 100);
196 | return { success: true };
197 | } else {
198 | const errorMessage = response?.error || 'Failed to switch model';
199 | const errorType = analyzeErrorType(errorMessage);
200 | await updateModelStatus('error', 0, errorMessage, errorType);
201 | throw new Error(errorMessage);
202 | }
203 | } catch (error: any) {
204 | console.error('Model switch failed:', error);
205 | const errorMessage = error.message || 'Unknown error';
206 | const errorType = analyzeErrorType(errorMessage);
207 | await updateModelStatus('error', 0, errorMessage, errorType);
208 | return { success: false, error: errorMessage };
209 | }
210 | }
211 |
212 | /**
213 | * Get model status
214 | */
215 | export async function handleGetModelStatus(): Promise<{
216 | success: boolean;
217 | status?: any;
218 | error?: string;
219 | }> {
220 | try {
221 | if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
222 | console.error('Background: chrome.storage.local is not available for status query');
223 | return {
224 | success: true,
225 | status: {
226 | initializationStatus: 'idle',
227 | downloadProgress: 0,
228 | isDownloading: false,
229 | lastUpdated: Date.now(),
230 | },
231 | };
232 | }
233 |
234 | const result = await chrome.storage.local.get(['modelState']);
235 | const modelState = result.modelState || {
236 | status: 'idle',
237 | downloadProgress: 0,
238 | isDownloading: false,
239 | lastUpdated: Date.now(),
240 | };
241 |
242 | return {
243 | success: true,
244 | status: {
245 | initializationStatus: modelState.status,
246 | downloadProgress: modelState.downloadProgress,
247 | isDownloading: modelState.isDownloading,
248 | lastUpdated: modelState.lastUpdated,
249 | errorMessage: modelState.errorMessage,
250 | errorType: modelState.errorType,
251 | },
252 | };
253 | } catch (error: any) {
254 | console.error('Failed to get model status:', error);
255 | return { success: false, error: error.message };
256 | }
257 | }
258 |
259 | /**
260 | * Update model status
261 | */
262 | export async function updateModelStatus(
263 | status: string,
264 | progress: number,
265 | errorMessage?: string,
266 | errorType?: string,
267 | ): Promise<void> {
268 | try {
269 | // Check if chrome.storage is available
270 | if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
271 | console.error('Background: chrome.storage.local is not available for status update');
272 | return;
273 | }
274 |
275 | const modelState = {
276 | status,
277 | downloadProgress: progress,
278 | isDownloading: status === 'downloading' || status === 'initializing',
279 | lastUpdated: Date.now(),
280 | errorMessage: errorMessage || '',
281 | errorType: errorType || '',
282 | };
283 | await chrome.storage.local.set({ modelState });
284 | } catch (error) {
285 | console.error('Failed to update model status:', error);
286 | }
287 | }
288 |
289 | /**
290 | * Handle model status updates from offscreen document
291 | */
292 | export async function handleUpdateModelStatus(
293 | modelState: any,
294 | ): Promise<{ success: boolean; error?: string }> {
295 | try {
296 | // Check if chrome.storage is available
297 | if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
298 | console.error('Background: chrome.storage.local is not available');
299 | return { success: false, error: 'chrome.storage.local is not available' };
300 | }
301 |
302 | await chrome.storage.local.set({ modelState });
303 | return { success: true };
304 | } catch (error: any) {
305 | console.error('Background: Failed to update model status:', error);
306 | return { success: false, error: error.message };
307 | }
308 | }
309 |
310 | /**
311 | * Analyze error type based on error message
312 | */
313 | function analyzeErrorType(errorMessage: string): 'network' | 'file' | 'unknown' {
314 | const message = errorMessage.toLowerCase();
315 |
316 | if (
317 | message.includes('network') ||
318 | message.includes('fetch') ||
319 | message.includes('timeout') ||
320 | message.includes('connection') ||
321 | message.includes('cors') ||
322 | message.includes('failed to fetch')
323 | ) {
324 | return 'network';
325 | }
326 |
327 | if (
328 | message.includes('corrupt') ||
329 | message.includes('invalid') ||
330 | message.includes('format') ||
331 | message.includes('parse') ||
332 | message.includes('decode') ||
333 | message.includes('onnx')
334 | ) {
335 | return 'file';
336 | }
337 |
338 | return 'unknown';
339 | }
340 |
341 | /**
342 | * Initialize semantic similarity module message listeners
343 | */
344 | export const initSemanticSimilarityListener = () => {
345 | chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
346 | if (message.type === BACKGROUND_MESSAGE_TYPES.SWITCH_SEMANTIC_MODEL) {
347 | handleModelSwitch(
348 | message.modelPreset,
349 | message.modelVersion,
350 | message.modelDimension,
351 | message.previousDimension,
352 | )
353 | .then((result: { success: boolean; error?: string }) => sendResponse(result))
354 | .catch((error: any) => sendResponse({ success: false, error: error.message }));
355 | return true;
356 | } else if (message.type === BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS) {
357 | handleGetModelStatus()
358 | .then((result: { success: boolean; status?: any; error?: string }) => sendResponse(result))
359 | .catch((error: any) => sendResponse({ success: false, error: error.message }));
360 | return true;
361 | } else if (message.type === BACKGROUND_MESSAGE_TYPES.UPDATE_MODEL_STATUS) {
362 | handleUpdateModelStatus(message.modelState)
363 | .then((result: { success: boolean; error?: string }) => sendResponse(result))
364 | .catch((error: any) => sendResponse({ success: false, error: error.message }));
365 | return true;
366 | } else if (message.type === BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE) {
367 | initializeDefaultSemanticEngine()
368 | .then(() => sendResponse({ success: true }))
369 | .catch((error: any) => sendResponse({ success: false, error: error.message }));
370 | return true;
371 | }
372 | });
373 | };
374 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/interactive-elements-helper.js:
--------------------------------------------------------------------------------
```javascript
1 | /* eslint-disable */
2 | // interactive-elements-helper.js
3 | // This script is injected into the page to find interactive elements.
4 | // Final version by Calvin, featuring a multi-layered fallback strategy
5 | // and comprehensive element support, built on a performant and reliable core.
6 |
7 | (function () {
8 | // Prevent re-initialization
9 | if (window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__) {
10 | return;
11 | }
12 | window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__ = true;
13 |
14 | /**
15 | * @typedef {Object} ElementInfo
16 | * @property {string} type - The type of the element (e.g., 'button', 'link').
17 | * @property {string} selector - A CSS selector to uniquely identify the element.
18 | * @property {string} text - The visible text or accessible name of the element.
19 | * @property {boolean} isInteractive - Whether the element is currently interactive.
20 | * @property {Object} [coordinates] - The coordinates of the element if requested.
21 | * @property {boolean} [disabled] - For elements that can be disabled.
22 | * @property {string} [href] - For links.
23 | * @property {boolean} [checked] - for checkboxes and radio buttons.
24 | */
25 |
26 | /**
27 | * Configuration for element types and their corresponding selectors.
28 | * Now more comprehensive with common ARIA roles.
29 | */
30 | const ELEMENT_CONFIG = {
31 | button: 'button, input[type="button"], input[type="submit"], [role="button"]',
32 | link: 'a[href], [role="link"]',
33 | input:
34 | 'input:not([type="button"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"])',
35 | checkbox: 'input[type="checkbox"], [role="checkbox"]',
36 | radio: 'input[type="radio"], [role="radio"]',
37 | textarea: 'textarea',
38 | select: 'select',
39 | tab: '[role="tab"]',
40 | // Generic interactive elements: combines tabindex, common roles, and explicit handlers.
41 | // This is the key to finding custom-built interactive components.
42 | interactive: `[onclick], [tabindex]:not([tabindex^="-"]), [role="menuitem"], [role="slider"], [role="option"], [role="treeitem"]`,
43 | };
44 |
45 | // A combined selector for ANY interactive element, used in the fallback logic.
46 | const ANY_INTERACTIVE_SELECTOR = Object.values(ELEMENT_CONFIG).join(', ');
47 |
48 | // --- Core Helper Functions ---
49 |
50 | /**
51 | * Checks if an element is genuinely visible on the page.
52 | * "Visible" means it's not styled with display:none, visibility:hidden, etc.
53 | * This check intentionally IGNORES whether the element is within the current viewport.
54 | * @param {Element} el The element to check.
55 | * @returns {boolean} True if the element is visible.
56 | */
57 | function isElementVisible(el) {
58 | if (!el || !el.isConnected) return false;
59 |
60 | const style = window.getComputedStyle(el);
61 | if (
62 | style.display === 'none' ||
63 | style.visibility === 'hidden' ||
64 | parseFloat(style.opacity) === 0
65 | ) {
66 | return false;
67 | }
68 |
69 | const rect = el.getBoundingClientRect();
70 | return rect.width > 0 || rect.height > 0 || el.tagName === 'A'; // Allow zero-size anchors as they can still be navigated
71 | }
72 |
73 | /**
74 | * Checks if an element is considered interactive (not disabled or hidden from accessibility).
75 | * @param {Element} el The element to check.
76 | * @returns {boolean} True if the element is interactive.
77 | */
78 | function isElementInteractive(el) {
79 | if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') {
80 | return false;
81 | }
82 | if (el.closest('[aria-hidden="true"]')) {
83 | return false;
84 | }
85 | return true;
86 | }
87 |
88 | /**
89 | * Generates a reasonably stable CSS selector for a given element.
90 | * @param {Element} el The element.
91 | * @returns {string} A CSS selector.
92 | */
93 | function generateSelector(el) {
94 | if (!(el instanceof Element)) return '';
95 |
96 | if (el.id) {
97 | const idSelector = `#${CSS.escape(el.id)}`;
98 | if (document.querySelectorAll(idSelector).length === 1) return idSelector;
99 | }
100 |
101 | for (const attr of ['data-testid', 'data-cy', 'name']) {
102 | const attrValue = el.getAttribute(attr);
103 | if (attrValue) {
104 | const attrSelector = `[${attr}="${CSS.escape(attrValue)}"]`;
105 | if (document.querySelectorAll(attrSelector).length === 1) return attrSelector;
106 | }
107 | }
108 |
109 | let path = '';
110 | let current = el;
111 | while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY') {
112 | let selector = current.tagName.toLowerCase();
113 | const parent = current.parentElement;
114 | if (parent) {
115 | const siblings = Array.from(parent.children).filter(
116 | (child) => child.tagName === current.tagName,
117 | );
118 | if (siblings.length > 1) {
119 | const index = siblings.indexOf(current) + 1;
120 | selector += `:nth-of-type(${index})`;
121 | }
122 | }
123 | path = path ? `${selector} > ${path}` : selector;
124 | current = parent;
125 | }
126 | return path ? `body > ${path}` : 'body';
127 | }
128 |
129 | /**
130 | * Finds the accessible name for an element (label, aria-label, etc.).
131 | * @param {Element} el The element.
132 | * @returns {string} The accessible name.
133 | */
134 | function getAccessibleName(el) {
135 | const labelledby = el.getAttribute('aria-labelledby');
136 | if (labelledby) {
137 | const labelElement = document.getElementById(labelledby);
138 | if (labelElement) return labelElement.textContent?.trim() || '';
139 | }
140 | const ariaLabel = el.getAttribute('aria-label');
141 | if (ariaLabel) return ariaLabel.trim();
142 | if (el.id) {
143 | const label = document.querySelector(`label[for="${el.id}"]`);
144 | if (label) return label.textContent?.trim() || '';
145 | }
146 | const parentLabel = el.closest('label');
147 | if (parentLabel) return parentLabel.textContent?.trim() || '';
148 | return (
149 | el.getAttribute('placeholder') ||
150 | el.getAttribute('value') ||
151 | el.textContent?.trim() ||
152 | el.getAttribute('title') ||
153 | ''
154 | );
155 | }
156 |
157 | /**
158 | * Simple subsequence matching for fuzzy search.
159 | * @param {string} text The text to search within.
160 | * @param {string} query The query subsequence.
161 | * @returns {boolean}
162 | */
163 | function fuzzyMatch(text, query) {
164 | if (!text || !query) return false;
165 | const lowerText = text.toLowerCase();
166 | const lowerQuery = query.toLowerCase();
167 | let textIndex = 0;
168 | let queryIndex = 0;
169 | while (textIndex < lowerText.length && queryIndex < lowerQuery.length) {
170 | if (lowerText[textIndex] === lowerQuery[queryIndex]) {
171 | queryIndex++;
172 | }
173 | textIndex++;
174 | }
175 | return queryIndex === lowerQuery.length;
176 | }
177 |
178 | /**
179 | * Creates the standardized info object for an element.
180 | * Modified to handle the new 'text' type from the final fallback.
181 | */
182 | function createElementInfo(el, type, includeCoordinates, isInteractiveOverride = null) {
183 | const isActuallyInteractive = isElementInteractive(el);
184 | const info = {
185 | type,
186 | selector: generateSelector(el),
187 | text: getAccessibleName(el) || el.textContent?.trim(),
188 | isInteractive: isInteractiveOverride !== null ? isInteractiveOverride : isActuallyInteractive,
189 | disabled: el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true',
190 | };
191 | if (includeCoordinates) {
192 | const rect = el.getBoundingClientRect();
193 | info.coordinates = {
194 | x: rect.left + rect.width / 2,
195 | y: rect.top + rect.height / 2,
196 | rect: {
197 | x: rect.x,
198 | y: rect.y,
199 | width: rect.width,
200 | height: rect.height,
201 | top: rect.top,
202 | right: rect.right,
203 | bottom: rect.bottom,
204 | left: rect.left,
205 | },
206 | };
207 | }
208 | return info;
209 | }
210 |
211 | /**
212 | * [CORE UTILITY] Finds interactive elements based on a set of types.
213 | * This is our high-performance Layer 1 search function.
214 | */
215 | function findInteractiveElements(options = {}) {
216 | const { textQuery, includeCoordinates = true, types = Object.keys(ELEMENT_CONFIG) } = options;
217 |
218 | const selectorsToFind = types
219 | .map((type) => ELEMENT_CONFIG[type])
220 | .filter(Boolean)
221 | .join(', ');
222 | if (!selectorsToFind) return [];
223 |
224 | const targetElements = Array.from(document.querySelectorAll(selectorsToFind));
225 | const uniqueElements = new Set(targetElements);
226 | const results = [];
227 |
228 | for (const el of uniqueElements) {
229 | if (!isElementVisible(el) || !isElementInteractive(el)) continue;
230 |
231 | const accessibleName = getAccessibleName(el);
232 | if (textQuery && !fuzzyMatch(accessibleName, textQuery)) continue;
233 |
234 | let elementType = 'unknown';
235 | for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
236 | if (el.matches(typeSelector)) {
237 | elementType = type;
238 | break;
239 | }
240 | }
241 | results.push(createElementInfo(el, elementType, includeCoordinates));
242 | }
243 | return results;
244 | }
245 |
246 | /**
247 | * [ORCHESTRATOR] The main entry point that implements the 3-layer fallback logic.
248 | * @param {object} options - The main search options.
249 | * @returns {ElementInfo[]}
250 | */
251 | function findElementsByTextWithFallback(options = {}) {
252 | const { textQuery, includeCoordinates = true } = options;
253 |
254 | if (!textQuery) {
255 | return findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
256 | }
257 |
258 | // --- Layer 1: High-reliability search for interactive elements matching text ---
259 | let results = findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
260 | if (results.length > 0) {
261 | return results;
262 | }
263 |
264 | // --- Layer 2: Find text, then find its interactive ancestor ---
265 | const lowerCaseText = textQuery.toLowerCase();
266 | const xPath = `//text()[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${lowerCaseText}')]`;
267 | const textNodes = document.evaluate(
268 | xPath,
269 | document,
270 | null,
271 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
272 | null,
273 | );
274 |
275 | const interactiveElements = new Set();
276 | if (textNodes.snapshotLength > 0) {
277 | for (let i = 0; i < textNodes.snapshotLength; i++) {
278 | const parentElement = textNodes.snapshotItem(i).parentElement;
279 | if (parentElement) {
280 | const interactiveAncestor = parentElement.closest(ANY_INTERACTIVE_SELECTOR);
281 | if (
282 | interactiveAncestor &&
283 | isElementVisible(interactiveAncestor) &&
284 | isElementInteractive(interactiveAncestor)
285 | ) {
286 | interactiveElements.add(interactiveAncestor);
287 | }
288 | }
289 | }
290 |
291 | if (interactiveElements.size > 0) {
292 | return Array.from(interactiveElements).map((el) => {
293 | let elementType = 'interactive';
294 | for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
295 | if (el.matches(typeSelector)) {
296 | elementType = type;
297 | break;
298 | }
299 | }
300 | return createElementInfo(el, elementType, includeCoordinates);
301 | });
302 | }
303 | }
304 |
305 | // --- Layer 3: Final fallback, return any element containing the text ---
306 | const leafElements = new Set();
307 | for (let i = 0; i < textNodes.snapshotLength; i++) {
308 | const parentElement = textNodes.snapshotItem(i).parentElement;
309 | if (parentElement && isElementVisible(parentElement)) {
310 | leafElements.add(parentElement);
311 | }
312 | }
313 |
314 | const finalElements = Array.from(leafElements).filter((el) => {
315 | return ![...leafElements].some((otherEl) => el !== otherEl && el.contains(otherEl));
316 | });
317 |
318 | return finalElements.map((el) => createElementInfo(el, 'text', includeCoordinates, true));
319 | }
320 |
321 | // --- Chrome Message Listener ---
322 | chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
323 | if (request.action === 'getInteractiveElements') {
324 | try {
325 | let elements;
326 | if (request.selector) {
327 | // If a selector is provided, bypass the text-based logic and use a direct query.
328 | const foundEls = Array.from(document.querySelectorAll(request.selector));
329 | elements = foundEls.map((el) =>
330 | createElementInfo(
331 | el,
332 | 'selected',
333 | request.includeCoordinates !== false,
334 | isElementInteractive(el),
335 | ),
336 | );
337 | } else {
338 | // Otherwise, use our powerful multi-layered text search
339 | elements = findElementsByTextWithFallback(request);
340 | }
341 | sendResponse({ success: true, elements });
342 | } catch (error) {
343 | console.error('Error in getInteractiveElements:', error);
344 | sendResponse({ success: false, error: error.message });
345 | }
346 | return true; // Async response
347 | } else if (request.action === 'chrome_get_interactive_elements_ping') {
348 | sendResponse({ status: 'pong' });
349 | return false;
350 | }
351 | });
352 |
353 | console.log('Interactive elements helper script loaded');
354 | })();
355 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/_locales/en/messages.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extensionName": {
3 | "message": "chrome-mcp-server",
4 | "description": "Extension name"
5 | },
6 | "extensionDescription": {
7 | "message": "Exposes browser capabilities with your own chrome",
8 | "description": "Extension description"
9 | },
10 | "nativeServerConfigLabel": {
11 | "message": "Native Server Configuration",
12 | "description": "Main section header for native server settings"
13 | },
14 | "semanticEngineLabel": {
15 | "message": "Semantic Engine",
16 | "description": "Main section header for semantic engine"
17 | },
18 | "embeddingModelLabel": {
19 | "message": "Embedding Model",
20 | "description": "Main section header for model selection"
21 | },
22 | "indexDataManagementLabel": {
23 | "message": "Index Data Management",
24 | "description": "Main section header for data management"
25 | },
26 | "modelCacheManagementLabel": {
27 | "message": "Model Cache Management",
28 | "description": "Main section header for cache management"
29 | },
30 | "statusLabel": {
31 | "message": "Status",
32 | "description": "Generic status label"
33 | },
34 | "runningStatusLabel": {
35 | "message": "Running Status",
36 | "description": "Server running status label"
37 | },
38 | "connectionStatusLabel": {
39 | "message": "Connection Status",
40 | "description": "Connection status label"
41 | },
42 | "lastUpdatedLabel": {
43 | "message": "Last Updated:",
44 | "description": "Last updated timestamp label"
45 | },
46 | "connectButton": {
47 | "message": "Connect",
48 | "description": "Connect button text"
49 | },
50 | "disconnectButton": {
51 | "message": "Disconnect",
52 | "description": "Disconnect button text"
53 | },
54 | "connectingStatus": {
55 | "message": "Connecting...",
56 | "description": "Connecting status message"
57 | },
58 | "connectedStatus": {
59 | "message": "Connected",
60 | "description": "Connected status message"
61 | },
62 | "disconnectedStatus": {
63 | "message": "Disconnected",
64 | "description": "Disconnected status message"
65 | },
66 | "detectingStatus": {
67 | "message": "Detecting...",
68 | "description": "Detecting status message"
69 | },
70 | "serviceRunningStatus": {
71 | "message": "Service Running (Port: $PORT$)",
72 | "description": "Service running with port number",
73 | "placeholders": {
74 | "port": {
75 | "content": "$1",
76 | "example": "12306"
77 | }
78 | }
79 | },
80 | "serviceNotConnectedStatus": {
81 | "message": "Service Not Connected",
82 | "description": "Service not connected status"
83 | },
84 | "connectedServiceNotStartedStatus": {
85 | "message": "Connected, Service Not Started",
86 | "description": "Connected but service not started status"
87 | },
88 | "mcpServerConfigLabel": {
89 | "message": "MCP Server Configuration",
90 | "description": "MCP server configuration section label"
91 | },
92 | "connectionPortLabel": {
93 | "message": "Connection Port",
94 | "description": "Connection port input label"
95 | },
96 | "refreshStatusButton": {
97 | "message": "Refresh Status",
98 | "description": "Refresh status button tooltip"
99 | },
100 | "copyConfigButton": {
101 | "message": "Copy Configuration",
102 | "description": "Copy configuration button text"
103 | },
104 | "retryButton": {
105 | "message": "Retry",
106 | "description": "Retry button text"
107 | },
108 | "cancelButton": {
109 | "message": "Cancel",
110 | "description": "Cancel button text"
111 | },
112 | "confirmButton": {
113 | "message": "Confirm",
114 | "description": "Confirm button text"
115 | },
116 | "saveButton": {
117 | "message": "Save",
118 | "description": "Save button text"
119 | },
120 | "closeButton": {
121 | "message": "Close",
122 | "description": "Close button text"
123 | },
124 | "resetButton": {
125 | "message": "Reset",
126 | "description": "Reset button text"
127 | },
128 | "initializingStatus": {
129 | "message": "Initializing...",
130 | "description": "Initializing progress message"
131 | },
132 | "processingStatus": {
133 | "message": "Processing...",
134 | "description": "Processing progress message"
135 | },
136 | "loadingStatus": {
137 | "message": "Loading...",
138 | "description": "Loading progress message"
139 | },
140 | "clearingStatus": {
141 | "message": "Clearing...",
142 | "description": "Clearing progress message"
143 | },
144 | "cleaningStatus": {
145 | "message": "Cleaning...",
146 | "description": "Cleaning progress message"
147 | },
148 | "downloadingStatus": {
149 | "message": "Downloading...",
150 | "description": "Downloading progress message"
151 | },
152 | "semanticEngineReadyStatus": {
153 | "message": "Semantic Engine Ready",
154 | "description": "Semantic engine ready status"
155 | },
156 | "semanticEngineInitializingStatus": {
157 | "message": "Semantic Engine Initializing...",
158 | "description": "Semantic engine initializing status"
159 | },
160 | "semanticEngineInitFailedStatus": {
161 | "message": "Semantic Engine Initialization Failed",
162 | "description": "Semantic engine initialization failed status"
163 | },
164 | "semanticEngineNotInitStatus": {
165 | "message": "Semantic Engine Not Initialized",
166 | "description": "Semantic engine not initialized status"
167 | },
168 | "initSemanticEngineButton": {
169 | "message": "Initialize Semantic Engine",
170 | "description": "Initialize semantic engine button text"
171 | },
172 | "reinitializeButton": {
173 | "message": "Reinitialize",
174 | "description": "Reinitialize button text"
175 | },
176 | "downloadingModelStatus": {
177 | "message": "Downloading Model... $PROGRESS$%",
178 | "description": "Model download progress with percentage",
179 | "placeholders": {
180 | "progress": {
181 | "content": "$1",
182 | "example": "50"
183 | }
184 | }
185 | },
186 | "switchingModelStatus": {
187 | "message": "Switching Model...",
188 | "description": "Model switching progress message"
189 | },
190 | "modelLoadedStatus": {
191 | "message": "Model Loaded",
192 | "description": "Model successfully loaded status"
193 | },
194 | "modelFailedStatus": {
195 | "message": "Model Failed to Load",
196 | "description": "Model failed to load status"
197 | },
198 | "lightweightModelDescription": {
199 | "message": "Lightweight Multilingual Model",
200 | "description": "Description for lightweight model option"
201 | },
202 | "betterThanSmallDescription": {
203 | "message": "Slightly larger than e5-small, but better performance",
204 | "description": "Description for medium model option"
205 | },
206 | "multilingualModelDescription": {
207 | "message": "Multilingual Semantic Model",
208 | "description": "Description for multilingual model option"
209 | },
210 | "fastPerformance": {
211 | "message": "Fast",
212 | "description": "Fast performance indicator"
213 | },
214 | "balancedPerformance": {
215 | "message": "Balanced",
216 | "description": "Balanced performance indicator"
217 | },
218 | "accuratePerformance": {
219 | "message": "Accurate",
220 | "description": "Accurate performance indicator"
221 | },
222 | "networkErrorMessage": {
223 | "message": "Network connection error, please check network and retry",
224 | "description": "Network connection error message"
225 | },
226 | "modelCorruptedErrorMessage": {
227 | "message": "Model file corrupted or incomplete, please retry download",
228 | "description": "Model corruption error message"
229 | },
230 | "unknownErrorMessage": {
231 | "message": "Unknown error, please check if your network can access HuggingFace",
232 | "description": "Unknown error fallback message"
233 | },
234 | "permissionDeniedErrorMessage": {
235 | "message": "Permission denied",
236 | "description": "Permission denied error message"
237 | },
238 | "timeoutErrorMessage": {
239 | "message": "Operation timed out",
240 | "description": "Timeout error message"
241 | },
242 | "indexedPagesLabel": {
243 | "message": "Indexed Pages",
244 | "description": "Number of indexed pages label"
245 | },
246 | "indexSizeLabel": {
247 | "message": "Index Size",
248 | "description": "Index size label"
249 | },
250 | "activeTabsLabel": {
251 | "message": "Active Tabs",
252 | "description": "Number of active tabs label"
253 | },
254 | "vectorDocumentsLabel": {
255 | "message": "Vector Documents",
256 | "description": "Number of vector documents label"
257 | },
258 | "cacheSizeLabel": {
259 | "message": "Cache Size",
260 | "description": "Cache size label"
261 | },
262 | "cacheEntriesLabel": {
263 | "message": "Cache Entries",
264 | "description": "Number of cache entries label"
265 | },
266 | "clearAllDataButton": {
267 | "message": "Clear All Data",
268 | "description": "Clear all data button text"
269 | },
270 | "clearAllCacheButton": {
271 | "message": "Clear All Cache",
272 | "description": "Clear all cache button text"
273 | },
274 | "cleanExpiredCacheButton": {
275 | "message": "Clean Expired Cache",
276 | "description": "Clean expired cache button text"
277 | },
278 | "exportDataButton": {
279 | "message": "Export Data",
280 | "description": "Export data button text"
281 | },
282 | "importDataButton": {
283 | "message": "Import Data",
284 | "description": "Import data button text"
285 | },
286 | "confirmClearDataTitle": {
287 | "message": "Confirm Clear Data",
288 | "description": "Clear data confirmation dialog title"
289 | },
290 | "settingsTitle": {
291 | "message": "Settings",
292 | "description": "Settings dialog title"
293 | },
294 | "aboutTitle": {
295 | "message": "About",
296 | "description": "About dialog title"
297 | },
298 | "helpTitle": {
299 | "message": "Help",
300 | "description": "Help dialog title"
301 | },
302 | "clearDataWarningMessage": {
303 | "message": "This operation will clear all indexed webpage content and vector data, including:",
304 | "description": "Clear data warning message"
305 | },
306 | "clearDataList1": {
307 | "message": "All webpage text content index",
308 | "description": "First item in clear data list"
309 | },
310 | "clearDataList2": {
311 | "message": "Vector embedding data",
312 | "description": "Second item in clear data list"
313 | },
314 | "clearDataList3": {
315 | "message": "Search history and cache",
316 | "description": "Third item in clear data list"
317 | },
318 | "clearDataIrreversibleWarning": {
319 | "message": "This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.",
320 | "description": "Irreversible operation warning"
321 | },
322 | "confirmClearButton": {
323 | "message": "Confirm Clear",
324 | "description": "Confirm clear action button"
325 | },
326 | "cacheDetailsLabel": {
327 | "message": "Cache Details",
328 | "description": "Cache details section label"
329 | },
330 | "noCacheDataMessage": {
331 | "message": "No cache data",
332 | "description": "No cache data available message"
333 | },
334 | "loadingCacheInfoStatus": {
335 | "message": "Loading cache information...",
336 | "description": "Loading cache information status"
337 | },
338 | "processingCacheStatus": {
339 | "message": "Processing cache...",
340 | "description": "Processing cache status"
341 | },
342 | "expiredLabel": {
343 | "message": "Expired",
344 | "description": "Expired item label"
345 | },
346 | "bookmarksBarLabel": {
347 | "message": "Bookmarks Bar",
348 | "description": "Bookmarks bar folder name"
349 | },
350 | "newTabLabel": {
351 | "message": "New Tab",
352 | "description": "New tab label"
353 | },
354 | "currentPageLabel": {
355 | "message": "Current Page",
356 | "description": "Current page label"
357 | },
358 | "menuLabel": {
359 | "message": "Menu",
360 | "description": "Menu accessibility label"
361 | },
362 | "navigationLabel": {
363 | "message": "Navigation",
364 | "description": "Navigation accessibility label"
365 | },
366 | "mainContentLabel": {
367 | "message": "Main Content",
368 | "description": "Main content accessibility label"
369 | },
370 | "languageSelectorLabel": {
371 | "message": "Language",
372 | "description": "Language selector label"
373 | },
374 | "themeLabel": {
375 | "message": "Theme",
376 | "description": "Theme selector label"
377 | },
378 | "lightTheme": {
379 | "message": "Light",
380 | "description": "Light theme option"
381 | },
382 | "darkTheme": {
383 | "message": "Dark",
384 | "description": "Dark theme option"
385 | },
386 | "autoTheme": {
387 | "message": "Auto",
388 | "description": "Auto theme option"
389 | },
390 | "advancedSettingsLabel": {
391 | "message": "Advanced Settings",
392 | "description": "Advanced settings section label"
393 | },
394 | "debugModeLabel": {
395 | "message": "Debug Mode",
396 | "description": "Debug mode toggle label"
397 | },
398 | "verboseLoggingLabel": {
399 | "message": "Verbose Logging",
400 | "description": "Verbose logging toggle label"
401 | },
402 | "successNotification": {
403 | "message": "Operation completed successfully",
404 | "description": "Generic success notification"
405 | },
406 | "warningNotification": {
407 | "message": "Warning: Please review before proceeding",
408 | "description": "Generic warning notification"
409 | },
410 | "infoNotification": {
411 | "message": "Information",
412 | "description": "Generic info notification"
413 | },
414 | "configCopiedNotification": {
415 | "message": "Configuration copied to clipboard",
416 | "description": "Configuration copied success message"
417 | },
418 | "dataClearedNotification": {
419 | "message": "Data cleared successfully",
420 | "description": "Data cleared success message"
421 | },
422 | "bytesUnit": {
423 | "message": "bytes",
424 | "description": "Bytes unit"
425 | },
426 | "kilobytesUnit": {
427 | "message": "KB",
428 | "description": "Kilobytes unit"
429 | },
430 | "megabytesUnit": {
431 | "message": "MB",
432 | "description": "Megabytes unit"
433 | },
434 | "gigabytesUnit": {
435 | "message": "GB",
436 | "description": "Gigabytes unit"
437 | },
438 | "itemsUnit": {
439 | "message": "items",
440 | "description": "Items count unit"
441 | },
442 | "pagesUnit": {
443 | "message": "pages",
444 | "description": "Pages count unit"
445 | }
446 | }
447 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/_locales/de/messages.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extensionName": {
3 | "message": "chrome-mcp-server",
4 | "description": "Erweiterungsname"
5 | },
6 | "extensionDescription": {
7 | "message": "Stellt Browser-Funktionen mit Ihrem eigenen Chrome zur Verfügung",
8 | "description": "Erweiterungsbeschreibung"
9 | },
10 | "nativeServerConfigLabel": {
11 | "message": "Native Server-Konfiguration",
12 | "description": "Hauptabschnittstitel für Native Server-Einstellungen"
13 | },
14 | "semanticEngineLabel": {
15 | "message": "Semantische Engine",
16 | "description": "Hauptabschnittstitel für semantische Engine"
17 | },
18 | "embeddingModelLabel": {
19 | "message": "Embedding-Modell",
20 | "description": "Hauptabschnittstitel für Modellauswahl"
21 | },
22 | "indexDataManagementLabel": {
23 | "message": "Index-Datenverwaltung",
24 | "description": "Hauptabschnittstitel für Datenverwaltung"
25 | },
26 | "modelCacheManagementLabel": {
27 | "message": "Modell-Cache-Verwaltung",
28 | "description": "Hauptabschnittstitel für Cache-Verwaltung"
29 | },
30 | "statusLabel": {
31 | "message": "Status",
32 | "description": "Allgemeines Statuslabel"
33 | },
34 | "runningStatusLabel": {
35 | "message": "Betriebsstatus",
36 | "description": "Server-Betriebsstatuslabel"
37 | },
38 | "connectionStatusLabel": {
39 | "message": "Verbindungsstatus",
40 | "description": "Verbindungsstatuslabel"
41 | },
42 | "lastUpdatedLabel": {
43 | "message": "Zuletzt aktualisiert:",
44 | "description": "Zeitstempel der letzten Aktualisierung"
45 | },
46 | "connectButton": {
47 | "message": "Verbinden",
48 | "description": "Verbinden-Schaltflächentext"
49 | },
50 | "disconnectButton": {
51 | "message": "Trennen",
52 | "description": "Trennen-Schaltflächentext"
53 | },
54 | "connectingStatus": {
55 | "message": "Verbindung wird hergestellt...",
56 | "description": "Verbindungsstatusmeldung"
57 | },
58 | "connectedStatus": {
59 | "message": "Verbunden",
60 | "description": "Verbunden-Statusmeldung"
61 | },
62 | "disconnectedStatus": {
63 | "message": "Getrennt",
64 | "description": "Getrennt-Statusmeldung"
65 | },
66 | "detectingStatus": {
67 | "message": "Erkennung läuft...",
68 | "description": "Erkennungsstatusmeldung"
69 | },
70 | "serviceRunningStatus": {
71 | "message": "Service läuft (Port: $PORT$)",
72 | "description": "Service läuft mit Portnummer",
73 | "placeholders": {
74 | "port": {
75 | "content": "$1",
76 | "example": "12306"
77 | }
78 | }
79 | },
80 | "serviceNotConnectedStatus": {
81 | "message": "Service nicht verbunden",
82 | "description": "Service nicht verbunden Status"
83 | },
84 | "connectedServiceNotStartedStatus": {
85 | "message": "Verbunden, Service nicht gestartet",
86 | "description": "Verbunden aber Service nicht gestartet Status"
87 | },
88 | "mcpServerConfigLabel": {
89 | "message": "MCP Server-Konfiguration",
90 | "description": "MCP Server-Konfigurationsabschnittslabel"
91 | },
92 | "connectionPortLabel": {
93 | "message": "Verbindungsport",
94 | "description": "Verbindungsport-Eingabelabel"
95 | },
96 | "refreshStatusButton": {
97 | "message": "Status aktualisieren",
98 | "description": "Status aktualisieren Schaltflächen-Tooltip"
99 | },
100 | "copyConfigButton": {
101 | "message": "Konfiguration kopieren",
102 | "description": "Konfiguration kopieren Schaltflächentext"
103 | },
104 | "retryButton": {
105 | "message": "Wiederholen",
106 | "description": "Wiederholen-Schaltflächentext"
107 | },
108 | "cancelButton": {
109 | "message": "Abbrechen",
110 | "description": "Abbrechen-Schaltflächentext"
111 | },
112 | "confirmButton": {
113 | "message": "Bestätigen",
114 | "description": "Bestätigen-Schaltflächentext"
115 | },
116 | "saveButton": {
117 | "message": "Speichern",
118 | "description": "Speichern-Schaltflächentext"
119 | },
120 | "closeButton": {
121 | "message": "Schließen",
122 | "description": "Schließen-Schaltflächentext"
123 | },
124 | "resetButton": {
125 | "message": "Zurücksetzen",
126 | "description": "Zurücksetzen-Schaltflächentext"
127 | },
128 | "initializingStatus": {
129 | "message": "Initialisierung...",
130 | "description": "Initialisierung-Fortschrittsmeldung"
131 | },
132 | "processingStatus": {
133 | "message": "Verarbeitung...",
134 | "description": "Verarbeitung-Fortschrittsmeldung"
135 | },
136 | "loadingStatus": {
137 | "message": "Wird geladen...",
138 | "description": "Ladefortschrittsmeldung"
139 | },
140 | "clearingStatus": {
141 | "message": "Wird geleert...",
142 | "description": "Leerungsfortschrittsmeldung"
143 | },
144 | "cleaningStatus": {
145 | "message": "Wird bereinigt...",
146 | "description": "Bereinigungsfortschrittsmeldung"
147 | },
148 | "downloadingStatus": {
149 | "message": "Wird heruntergeladen...",
150 | "description": "Download-Fortschrittsmeldung"
151 | },
152 | "semanticEngineReadyStatus": {
153 | "message": "Semantische Engine bereit",
154 | "description": "Semantische Engine bereit Status"
155 | },
156 | "semanticEngineInitializingStatus": {
157 | "message": "Semantische Engine wird initialisiert...",
158 | "description": "Semantische Engine Initialisierungsstatus"
159 | },
160 | "semanticEngineInitFailedStatus": {
161 | "message": "Initialisierung der semantischen Engine fehlgeschlagen",
162 | "description": "Semantische Engine Initialisierung fehlgeschlagen Status"
163 | },
164 | "semanticEngineNotInitStatus": {
165 | "message": "Semantische Engine nicht initialisiert",
166 | "description": "Semantische Engine nicht initialisiert Status"
167 | },
168 | "initSemanticEngineButton": {
169 | "message": "Semantische Engine initialisieren",
170 | "description": "Semantische Engine initialisieren Schaltflächentext"
171 | },
172 | "reinitializeButton": {
173 | "message": "Neu initialisieren",
174 | "description": "Neu initialisieren Schaltflächentext"
175 | },
176 | "downloadingModelStatus": {
177 | "message": "Modell wird heruntergeladen... $PROGRESS$%",
178 | "description": "Modell-Download-Fortschritt mit Prozentsatz",
179 | "placeholders": {
180 | "progress": {
181 | "content": "$1",
182 | "example": "50"
183 | }
184 | }
185 | },
186 | "switchingModelStatus": {
187 | "message": "Modell wird gewechselt...",
188 | "description": "Modellwechsel-Fortschrittsmeldung"
189 | },
190 | "modelLoadedStatus": {
191 | "message": "Modell geladen",
192 | "description": "Modell erfolgreich geladen Status"
193 | },
194 | "modelFailedStatus": {
195 | "message": "Modell konnte nicht geladen werden",
196 | "description": "Modell-Ladefehler Status"
197 | },
198 | "lightweightModelDescription": {
199 | "message": "Leichtgewichtiges mehrsprachiges Modell",
200 | "description": "Beschreibung für leichtgewichtige Modelloption"
201 | },
202 | "betterThanSmallDescription": {
203 | "message": "Etwas größer als e5-small, aber bessere Leistung",
204 | "description": "Beschreibung für mittlere Modelloption"
205 | },
206 | "multilingualModelDescription": {
207 | "message": "Mehrsprachiges semantisches Modell",
208 | "description": "Beschreibung für mehrsprachige Modelloption"
209 | },
210 | "fastPerformance": {
211 | "message": "Schnell",
212 | "description": "Schnelle Leistungsanzeige"
213 | },
214 | "balancedPerformance": {
215 | "message": "Ausgewogen",
216 | "description": "Ausgewogene Leistungsanzeige"
217 | },
218 | "accuratePerformance": {
219 | "message": "Genau",
220 | "description": "Genaue Leistungsanzeige"
221 | },
222 | "networkErrorMessage": {
223 | "message": "Netzwerkverbindungsfehler, bitte Netzwerk prüfen und erneut versuchen",
224 | "description": "Netzwerkverbindungsfehlermeldung"
225 | },
226 | "modelCorruptedErrorMessage": {
227 | "message": "Modelldatei beschädigt oder unvollständig, bitte Download wiederholen",
228 | "description": "Modell-Beschädigungsfehlermeldung"
229 | },
230 | "unknownErrorMessage": {
231 | "message": "Unbekannter Fehler, bitte prüfen Sie, ob Ihr Netzwerk auf HuggingFace zugreifen kann",
232 | "description": "Unbekannte Fehler-Rückfallmeldung"
233 | },
234 | "permissionDeniedErrorMessage": {
235 | "message": "Zugriff verweigert",
236 | "description": "Zugriff verweigert Fehlermeldung"
237 | },
238 | "timeoutErrorMessage": {
239 | "message": "Zeitüberschreitung",
240 | "description": "Zeitüberschreitungsfehlermeldung"
241 | },
242 | "indexedPagesLabel": {
243 | "message": "Indizierte Seiten",
244 | "description": "Anzahl indizierter Seiten Label"
245 | },
246 | "indexSizeLabel": {
247 | "message": "Indexgröße",
248 | "description": "Indexgröße Label"
249 | },
250 | "activeTabsLabel": {
251 | "message": "Aktive Tabs",
252 | "description": "Anzahl aktiver Tabs Label"
253 | },
254 | "vectorDocumentsLabel": {
255 | "message": "Vektordokumente",
256 | "description": "Anzahl Vektordokumente Label"
257 | },
258 | "cacheSizeLabel": {
259 | "message": "Cache-Größe",
260 | "description": "Cache-Größe Label"
261 | },
262 | "cacheEntriesLabel": {
263 | "message": "Cache-Einträge",
264 | "description": "Anzahl Cache-Einträge Label"
265 | },
266 | "clearAllDataButton": {
267 | "message": "Alle Daten löschen",
268 | "description": "Alle Daten löschen Schaltflächentext"
269 | },
270 | "clearAllCacheButton": {
271 | "message": "Gesamten Cache löschen",
272 | "description": "Gesamten Cache löschen Schaltflächentext"
273 | },
274 | "cleanExpiredCacheButton": {
275 | "message": "Abgelaufenen Cache bereinigen",
276 | "description": "Abgelaufenen Cache bereinigen Schaltflächentext"
277 | },
278 | "exportDataButton": {
279 | "message": "Daten exportieren",
280 | "description": "Daten exportieren Schaltflächentext"
281 | },
282 | "importDataButton": {
283 | "message": "Daten importieren",
284 | "description": "Daten importieren Schaltflächentext"
285 | },
286 | "confirmClearDataTitle": {
287 | "message": "Datenlöschung bestätigen",
288 | "description": "Datenlöschung bestätigen Dialogtitel"
289 | },
290 | "settingsTitle": {
291 | "message": "Einstellungen",
292 | "description": "Einstellungen Dialogtitel"
293 | },
294 | "aboutTitle": {
295 | "message": "Über",
296 | "description": "Über Dialogtitel"
297 | },
298 | "helpTitle": {
299 | "message": "Hilfe",
300 | "description": "Hilfe Dialogtitel"
301 | },
302 | "clearDataWarningMessage": {
303 | "message": "Diese Aktion löscht alle indizierten Webseiteninhalte und Vektordaten, einschließlich:",
304 | "description": "Datenlöschung Warnmeldung"
305 | },
306 | "clearDataList1": {
307 | "message": "Alle Webseitentextinhaltsindizes",
308 | "description": "Erster Punkt in Datenlöschungsliste"
309 | },
310 | "clearDataList2": {
311 | "message": "Vektor-Embedding-Daten",
312 | "description": "Zweiter Punkt in Datenlöschungsliste"
313 | },
314 | "clearDataList3": {
315 | "message": "Suchverlauf und Cache",
316 | "description": "Dritter Punkt in Datenlöschungsliste"
317 | },
318 | "clearDataIrreversibleWarning": {
319 | "message": "Diese Aktion ist unwiderruflich! Nach dem Löschen müssen Sie Webseiten erneut durchsuchen, um den Index neu aufzubauen.",
320 | "description": "Unwiderrufliche Aktion Warnung"
321 | },
322 | "confirmClearButton": {
323 | "message": "Löschung bestätigen",
324 | "description": "Löschung bestätigen Aktionsschaltfläche"
325 | },
326 | "cacheDetailsLabel": {
327 | "message": "Cache-Details",
328 | "description": "Cache-Details Abschnittslabel"
329 | },
330 | "noCacheDataMessage": {
331 | "message": "Keine Cache-Daten vorhanden",
332 | "description": "Keine Cache-Daten verfügbar Meldung"
333 | },
334 | "loadingCacheInfoStatus": {
335 | "message": "Cache-Informationen werden geladen...",
336 | "description": "Cache-Informationen laden Status"
337 | },
338 | "processingCacheStatus": {
339 | "message": "Cache wird verarbeitet...",
340 | "description": "Cache verarbeiten Status"
341 | },
342 | "expiredLabel": {
343 | "message": "Abgelaufen",
344 | "description": "Abgelaufenes Element Label"
345 | },
346 | "bookmarksBarLabel": {
347 | "message": "Lesezeichenleiste",
348 | "description": "Lesezeichenleiste Ordnername"
349 | },
350 | "newTabLabel": {
351 | "message": "Neuer Tab",
352 | "description": "Neuer Tab Label"
353 | },
354 | "currentPageLabel": {
355 | "message": "Aktuelle Seite",
356 | "description": "Aktuelle Seite Label"
357 | },
358 | "menuLabel": {
359 | "message": "Menü",
360 | "description": "Menü Barrierefreiheitslabel"
361 | },
362 | "navigationLabel": {
363 | "message": "Navigation",
364 | "description": "Navigation Barrierefreiheitslabel"
365 | },
366 | "mainContentLabel": {
367 | "message": "Hauptinhalt",
368 | "description": "Hauptinhalt Barrierefreiheitslabel"
369 | },
370 | "languageSelectorLabel": {
371 | "message": "Sprache",
372 | "description": "Sprachauswahl Label"
373 | },
374 | "themeLabel": {
375 | "message": "Design",
376 | "description": "Design-Auswahl Label"
377 | },
378 | "lightTheme": {
379 | "message": "Hell",
380 | "description": "Helles Design Option"
381 | },
382 | "darkTheme": {
383 | "message": "Dunkel",
384 | "description": "Dunkles Design Option"
385 | },
386 | "autoTheme": {
387 | "message": "Automatisch",
388 | "description": "Automatisches Design Option"
389 | },
390 | "advancedSettingsLabel": {
391 | "message": "Erweiterte Einstellungen",
392 | "description": "Erweiterte Einstellungen Abschnittslabel"
393 | },
394 | "debugModeLabel": {
395 | "message": "Debug-Modus",
396 | "description": "Debug-Modus Umschalter Label"
397 | },
398 | "verboseLoggingLabel": {
399 | "message": "Ausführliche Protokollierung",
400 | "description": "Ausführliche Protokollierung Umschalter Label"
401 | },
402 | "successNotification": {
403 | "message": "Vorgang erfolgreich abgeschlossen",
404 | "description": "Allgemeine Erfolgsmeldung"
405 | },
406 | "warningNotification": {
407 | "message": "Warnung: Bitte prüfen Sie vor dem Fortfahren",
408 | "description": "Allgemeine Warnmeldung"
409 | },
410 | "infoNotification": {
411 | "message": "Information",
412 | "description": "Allgemeine Informationsmeldung"
413 | },
414 | "configCopiedNotification": {
415 | "message": "Konfiguration in Zwischenablage kopiert",
416 | "description": "Konfiguration kopiert Erfolgsmeldung"
417 | },
418 | "dataClearedNotification": {
419 | "message": "Daten erfolgreich gelöscht",
420 | "description": "Daten gelöscht Erfolgsmeldung"
421 | },
422 | "bytesUnit": {
423 | "message": "Bytes",
424 | "description": "Bytes Einheit"
425 | },
426 | "kilobytesUnit": {
427 | "message": "KB",
428 | "description": "Kilobytes Einheit"
429 | },
430 | "megabytesUnit": {
431 | "message": "MB",
432 | "description": "Megabytes Einheit"
433 | },
434 | "gigabytesUnit": {
435 | "message": "GB",
436 | "description": "Gigabytes Einheit"
437 | },
438 | "itemsUnit": {
439 | "message": "Elemente",
440 | "description": "Elemente Zähleinheit"
441 | },
442 | "pagesUnit": {
443 | "message": "Seiten",
444 | "description": "Seiten Zähleinheit"
445 | }
446 | }
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/simd-math-engine.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * SIMD-optimized mathematical computation engine
3 | * Uses WebAssembly + SIMD instructions to accelerate vector calculations
4 | */
5 |
6 | interface SIMDMathWasm {
7 | free(): void;
8 | cosine_similarity(vec_a: Float32Array, vec_b: Float32Array): number;
9 | batch_similarity(vectors: Float32Array, query: Float32Array, vector_dim: number): Float32Array;
10 | similarity_matrix(
11 | vectors_a: Float32Array,
12 | vectors_b: Float32Array,
13 | vector_dim: number,
14 | ): Float32Array;
15 | }
16 |
17 | interface WasmModule {
18 | SIMDMath: new () => SIMDMathWasm;
19 | memory: WebAssembly.Memory;
20 | default: (module_or_path?: any) => Promise<any>;
21 | }
22 |
23 | export class SIMDMathEngine {
24 | private wasmModule: WasmModule | null = null;
25 | private simdMath: SIMDMathWasm | null = null;
26 | private isInitialized = false;
27 | private isInitializing = false;
28 | private initPromise: Promise<void> | null = null;
29 |
30 | private alignedBufferPool: Map<number, Float32Array[]> = new Map();
31 | private maxPoolSize = 5;
32 |
33 | async initialize(): Promise<void> {
34 | if (this.isInitialized) return;
35 | if (this.isInitializing && this.initPromise) return this.initPromise;
36 |
37 | this.isInitializing = true;
38 | this.initPromise = this._doInitialize().finally(() => {
39 | this.isInitializing = false;
40 | });
41 |
42 | return this.initPromise;
43 | }
44 |
45 | private async _doInitialize(): Promise<void> {
46 | try {
47 | console.log('SIMDMathEngine: Initializing WebAssembly module...');
48 |
49 | const wasmUrl = chrome.runtime.getURL('workers/simd_math.js');
50 | const wasmModule = await import(wasmUrl);
51 |
52 | const wasmInstance = await wasmModule.default();
53 |
54 | this.wasmModule = {
55 | SIMDMath: wasmModule.SIMDMath,
56 | memory: wasmInstance.memory,
57 | default: wasmModule.default,
58 | };
59 |
60 | this.simdMath = new this.wasmModule.SIMDMath();
61 |
62 | this.isInitialized = true;
63 | console.log('SIMDMathEngine: WebAssembly module initialized successfully');
64 | } catch (error) {
65 | console.error('SIMDMathEngine: Failed to initialize WebAssembly module:', error);
66 | this.isInitialized = false;
67 | throw error;
68 | }
69 | }
70 |
71 | /**
72 | * Get aligned buffer (16-byte aligned, suitable for SIMD)
73 | */
74 | private getAlignedBuffer(size: number): Float32Array {
75 | if (!this.alignedBufferPool.has(size)) {
76 | this.alignedBufferPool.set(size, []);
77 | }
78 |
79 | const pool = this.alignedBufferPool.get(size)!;
80 | if (pool.length > 0) {
81 | return pool.pop()!;
82 | }
83 |
84 | // Create 16-byte aligned buffer
85 | const buffer = new ArrayBuffer(size * 4 + 15);
86 | const alignedOffset = (16 - (buffer.byteLength % 16)) % 16;
87 | return new Float32Array(buffer, alignedOffset, size);
88 | }
89 |
90 | /**
91 | * Release aligned buffer back to pool
92 | */
93 | private releaseAlignedBuffer(buffer: Float32Array): void {
94 | const size = buffer.length;
95 | const pool = this.alignedBufferPool.get(size);
96 | if (pool && pool.length < this.maxPoolSize) {
97 | buffer.fill(0); // Clear to zero
98 | pool.push(buffer);
99 | }
100 | }
101 |
102 | /**
103 | * Check if vector is already aligned
104 | */
105 | private isAligned(array: Float32Array): boolean {
106 | return array.byteOffset % 16 === 0;
107 | }
108 |
109 | /**
110 | * Ensure vector alignment, create aligned copy if not aligned
111 | */
112 | private ensureAligned(array: Float32Array): { aligned: Float32Array; needsRelease: boolean } {
113 | if (this.isAligned(array)) {
114 | return { aligned: array, needsRelease: false };
115 | }
116 |
117 | const aligned = this.getAlignedBuffer(array.length);
118 | aligned.set(array);
119 | return { aligned, needsRelease: true };
120 | }
121 |
122 | /**
123 | * SIMD-optimized cosine similarity calculation
124 | */
125 | async cosineSimilarity(vecA: Float32Array, vecB: Float32Array): Promise<number> {
126 | if (!this.isInitialized) {
127 | await this.initialize();
128 | }
129 |
130 | if (!this.simdMath) {
131 | throw new Error('SIMD math engine not initialized');
132 | }
133 |
134 | // Ensure vector alignment
135 | const { aligned: alignedA, needsRelease: releaseA } = this.ensureAligned(vecA);
136 | const { aligned: alignedB, needsRelease: releaseB } = this.ensureAligned(vecB);
137 |
138 | try {
139 | const result = this.simdMath.cosine_similarity(alignedA, alignedB);
140 | return result;
141 | } finally {
142 | // Release temporary buffers
143 | if (releaseA) this.releaseAlignedBuffer(alignedA);
144 | if (releaseB) this.releaseAlignedBuffer(alignedB);
145 | }
146 | }
147 |
148 | /**
149 | * Batch similarity calculation
150 | */
151 | async batchSimilarity(vectors: Float32Array[], query: Float32Array): Promise<number[]> {
152 | if (!this.isInitialized) {
153 | await this.initialize();
154 | }
155 |
156 | if (!this.simdMath) {
157 | throw new Error('SIMD math engine not initialized');
158 | }
159 |
160 | const vectorDim = query.length;
161 | const numVectors = vectors.length;
162 |
163 | // Pack all vectors into contiguous memory layout
164 | const packedVectors = this.getAlignedBuffer(numVectors * vectorDim);
165 | const { aligned: alignedQuery, needsRelease: releaseQuery } = this.ensureAligned(query);
166 |
167 | try {
168 | // Copy vector data
169 | let offset = 0;
170 | for (const vector of vectors) {
171 | packedVectors.set(vector, offset);
172 | offset += vectorDim;
173 | }
174 |
175 | // Batch calculation
176 | const results = this.simdMath.batch_similarity(packedVectors, alignedQuery, vectorDim);
177 | return Array.from(results);
178 | } finally {
179 | this.releaseAlignedBuffer(packedVectors);
180 | if (releaseQuery) this.releaseAlignedBuffer(alignedQuery);
181 | }
182 | }
183 |
184 | /**
185 | * Similarity matrix calculation
186 | */
187 | async similarityMatrix(vectorsA: Float32Array[], vectorsB: Float32Array[]): Promise<number[][]> {
188 | if (!this.isInitialized) {
189 | await this.initialize();
190 | }
191 |
192 | if (!this.simdMath || vectorsA.length === 0 || vectorsB.length === 0) {
193 | return [];
194 | }
195 |
196 | const vectorDim = vectorsA[0].length;
197 | const numA = vectorsA.length;
198 | const numB = vectorsB.length;
199 |
200 | // Pack vectors
201 | const packedA = this.getAlignedBuffer(numA * vectorDim);
202 | const packedB = this.getAlignedBuffer(numB * vectorDim);
203 |
204 | try {
205 | // Copy data
206 | let offsetA = 0;
207 | for (const vector of vectorsA) {
208 | packedA.set(vector, offsetA);
209 | offsetA += vectorDim;
210 | }
211 |
212 | let offsetB = 0;
213 | for (const vector of vectorsB) {
214 | packedB.set(vector, offsetB);
215 | offsetB += vectorDim;
216 | }
217 |
218 | // Calculate matrix
219 | const flatResults = this.simdMath.similarity_matrix(packedA, packedB, vectorDim);
220 |
221 | // Convert to 2D array
222 | const matrix: number[][] = [];
223 | for (let i = 0; i < numA; i++) {
224 | const row: number[] = [];
225 | for (let j = 0; j < numB; j++) {
226 | row.push(flatResults[i * numB + j]);
227 | }
228 | matrix.push(row);
229 | }
230 |
231 | return matrix;
232 | } finally {
233 | this.releaseAlignedBuffer(packedA);
234 | this.releaseAlignedBuffer(packedB);
235 | }
236 | }
237 |
238 | /**
239 | * Check SIMD support
240 | */
241 | static async checkSIMDSupport(): Promise<boolean> {
242 | try {
243 | console.log('SIMDMathEngine: Checking SIMD support...');
244 |
245 | // Get browser information
246 | const userAgent = navigator.userAgent;
247 | const browserInfo = SIMDMathEngine.getBrowserInfo();
248 | console.log('Browser info:', browserInfo);
249 | console.log('User Agent:', userAgent);
250 |
251 | // Check WebAssembly basic support
252 | if (typeof WebAssembly !== 'object') {
253 | console.log('WebAssembly not supported');
254 | return false;
255 | }
256 | console.log('✅ WebAssembly basic support: OK');
257 |
258 | // Check WebAssembly.validate method
259 | if (typeof WebAssembly.validate !== 'function') {
260 | console.log('❌ WebAssembly.validate not available');
261 | return false;
262 | }
263 | console.log('✅ WebAssembly.validate: OK');
264 |
265 | // Test basic WebAssembly module validation
266 | const basicWasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
267 | const basicValid = WebAssembly.validate(basicWasm);
268 | console.log('✅ Basic WASM validation:', basicValid);
269 |
270 | // Check WebAssembly SIMD support - using correct SIMD test module
271 | console.log('Testing SIMD WASM module...');
272 |
273 | // Method 1: Use standard SIMD detection bytecode
274 | let wasmSIMDSupported = false;
275 | try {
276 | // This is a minimal SIMD module containing v128.const instruction
277 | const simdWasm = new Uint8Array([
278 | 0x00,
279 | 0x61,
280 | 0x73,
281 | 0x6d, // WASM magic
282 | 0x01,
283 | 0x00,
284 | 0x00,
285 | 0x00, // version
286 | 0x01,
287 | 0x05,
288 | 0x01, // type section
289 | 0x60,
290 | 0x00,
291 | 0x01,
292 | 0x7b, // function type: () -> v128
293 | 0x03,
294 | 0x02,
295 | 0x01,
296 | 0x00, // function section
297 | 0x0a,
298 | 0x0a,
299 | 0x01, // code section
300 | 0x08,
301 | 0x00, // function body
302 | 0xfd,
303 | 0x0c, // v128.const
304 | 0x00,
305 | 0x00,
306 | 0x00,
307 | 0x00,
308 | 0x00,
309 | 0x00,
310 | 0x00,
311 | 0x00,
312 | 0x00,
313 | 0x00,
314 | 0x00,
315 | 0x00,
316 | 0x00,
317 | 0x00,
318 | 0x00,
319 | 0x00,
320 | 0x0b, // end
321 | ]);
322 | wasmSIMDSupported = WebAssembly.validate(simdWasm);
323 | console.log('Method 1 - Standard SIMD test result:', wasmSIMDSupported);
324 | } catch (error) {
325 | console.log('Method 1 failed:', error);
326 | }
327 |
328 | // Method 2: If method 1 fails, try simpler SIMD instruction
329 | if (!wasmSIMDSupported) {
330 | try {
331 | // Test using i32x4.splat instruction
332 | const simpleSimdWasm = new Uint8Array([
333 | 0x00,
334 | 0x61,
335 | 0x73,
336 | 0x6d, // WASM magic
337 | 0x01,
338 | 0x00,
339 | 0x00,
340 | 0x00, // version
341 | 0x01,
342 | 0x06,
343 | 0x01, // type section
344 | 0x60,
345 | 0x01,
346 | 0x7f,
347 | 0x01,
348 | 0x7b, // function type: (i32) -> v128
349 | 0x03,
350 | 0x02,
351 | 0x01,
352 | 0x00, // function section
353 | 0x0a,
354 | 0x07,
355 | 0x01, // code section
356 | 0x05,
357 | 0x00, // function body
358 | 0x20,
359 | 0x00, // local.get 0
360 | 0xfd,
361 | 0x0d, // i32x4.splat
362 | 0x0b, // end
363 | ]);
364 | wasmSIMDSupported = WebAssembly.validate(simpleSimdWasm);
365 | console.log('Method 2 - Simple SIMD test result:', wasmSIMDSupported);
366 | } catch (error) {
367 | console.log('Method 2 failed:', error);
368 | }
369 | }
370 |
371 | // Method 3: If previous methods fail, try detecting specific SIMD features
372 | if (!wasmSIMDSupported) {
373 | try {
374 | // Check if SIMD feature flags are supported
375 | const featureTest = WebAssembly.validate(
376 | new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
377 | );
378 |
379 | if (featureTest) {
380 | // In Chrome, if basic WebAssembly works and version >= 91, SIMD is usually available
381 | const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
382 | if (chromeMatch && parseInt(chromeMatch[1]) >= 91) {
383 | console.log('Method 3 - Chrome version check: SIMD should be available');
384 | wasmSIMDSupported = true;
385 | }
386 | }
387 | } catch (error) {
388 | console.log('Method 3 failed:', error);
389 | }
390 | }
391 |
392 | // Output final result
393 | if (!wasmSIMDSupported) {
394 | console.log('❌ SIMD not supported. Browser requirements:');
395 | console.log('- Chrome 91+, Firefox 89+, Safari 16.4+, Edge 91+');
396 | console.log('Your browser should support SIMD. Possible issues:');
397 | console.log('1. Extension context limitations');
398 | console.log('2. Security policies');
399 | console.log('3. Feature flags disabled');
400 | } else {
401 | console.log('✅ SIMD supported!');
402 | }
403 |
404 | return wasmSIMDSupported;
405 | } catch (error: any) {
406 | console.error('SIMD support check failed:', error);
407 | if (error instanceof Error) {
408 | console.error('Error details:', {
409 | name: error.name,
410 | message: error.message,
411 | stack: error.stack,
412 | });
413 | }
414 | return false;
415 | }
416 | }
417 |
418 | /**
419 | * Get browser information
420 | */
421 | static getBrowserInfo(): { name: string; version: string; supported: boolean } {
422 | const userAgent = navigator.userAgent;
423 | let browserName = 'Unknown';
424 | let version = 'Unknown';
425 | let supported = false;
426 |
427 | // Chrome
428 | if (userAgent.includes('Chrome/')) {
429 | browserName = 'Chrome';
430 | const match = userAgent.match(/Chrome\/(\d+)/);
431 | if (match) {
432 | version = match[1];
433 | supported = parseInt(version) >= 91;
434 | }
435 | }
436 | // Firefox
437 | else if (userAgent.includes('Firefox/')) {
438 | browserName = 'Firefox';
439 | const match = userAgent.match(/Firefox\/(\d+)/);
440 | if (match) {
441 | version = match[1];
442 | supported = parseInt(version) >= 89;
443 | }
444 | }
445 | // Safari
446 | else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) {
447 | browserName = 'Safari';
448 | const match = userAgent.match(/Version\/(\d+\.\d+)/);
449 | if (match) {
450 | version = match[1];
451 | const versionNum = parseFloat(version);
452 | supported = versionNum >= 16.4;
453 | }
454 | }
455 | // Edge
456 | else if (userAgent.includes('Edg/')) {
457 | browserName = 'Edge';
458 | const match = userAgent.match(/Edg\/(\d+)/);
459 | if (match) {
460 | version = match[1];
461 | supported = parseInt(version) >= 91;
462 | }
463 | }
464 |
465 | return { name: browserName, version, supported };
466 | }
467 |
468 | getStats() {
469 | return {
470 | isInitialized: this.isInitialized,
471 | isInitializing: this.isInitializing,
472 | bufferPoolStats: Array.from(this.alignedBufferPool.entries()).map(([size, buffers]) => ({
473 | size,
474 | pooled: buffers.length,
475 | maxPoolSize: this.maxPoolSize,
476 | })),
477 | };
478 | }
479 |
480 | dispose(): void {
481 | if (this.simdMath) {
482 | try {
483 | this.simdMath.free();
484 | } catch (error) {
485 | console.warn('Failed to free SIMD math instance:', error);
486 | }
487 | this.simdMath = null;
488 | }
489 |
490 | this.alignedBufferPool.clear();
491 | this.wasmModule = null;
492 | this.isInitialized = false;
493 | this.isInitializing = false;
494 | this.initPromise = null;
495 | }
496 | }
497 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/screenshot.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { BaseBrowserToolExecutor } from '../base-browser';
3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
4 | import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
5 | import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
6 | import {
7 | canvasToDataURL,
8 | createImageBitmapFromUrl,
9 | cropAndResizeImage,
10 | stitchImages,
11 | compressImage,
12 | } from '../../../../utils/image-utils';
13 |
14 | // Screenshot-specific constants
15 | const SCREENSHOT_CONSTANTS = {
16 | SCROLL_DELAY_MS: 350, // Time to wait after scroll for rendering and lazy loading
17 | CAPTURE_STITCH_DELAY_MS: 50, // Small delay between captures in a scroll sequence
18 | MAX_CAPTURE_PARTS: 50, // Maximum number of parts to capture (for infinite scroll pages)
19 | MAX_CAPTURE_HEIGHT_PX: 50000, // Maximum height in pixels to capture
20 | PIXEL_TOLERANCE: 1,
21 | SCRIPT_INIT_DELAY: 100, // Delay for script initialization
22 | } as {
23 | readonly SCROLL_DELAY_MS: number;
24 | CAPTURE_STITCH_DELAY_MS: number; // This one is mutable
25 | readonly MAX_CAPTURE_PARTS: number;
26 | readonly MAX_CAPTURE_HEIGHT_PX: number;
27 | readonly PIXEL_TOLERANCE: number;
28 | readonly SCRIPT_INIT_DELAY: number;
29 | };
30 |
31 | SCREENSHOT_CONSTANTS["CAPTURE_STITCH_DELAY_MS"] = Math.max(1000 / chrome.tabs.MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND - SCREENSHOT_CONSTANTS.SCROLL_DELAY_MS, SCREENSHOT_CONSTANTS.CAPTURE_STITCH_DELAY_MS)
32 |
33 | interface ScreenshotToolParams {
34 | name: string;
35 | selector?: string;
36 | width?: number;
37 | height?: number;
38 | storeBase64?: boolean;
39 | fullPage?: boolean;
40 | savePng?: boolean;
41 | maxHeight?: number; // Maximum height to capture in pixels (for infinite scroll pages)
42 | }
43 |
44 | /**
45 | * Tool for capturing screenshots of web pages
46 | */
47 | class ScreenshotTool extends BaseBrowserToolExecutor {
48 | name = TOOL_NAMES.BROWSER.SCREENSHOT;
49 |
50 | /**
51 | * Execute screenshot operation
52 | */
53 | async execute(args: ScreenshotToolParams): Promise<ToolResult> {
54 | const {
55 | name = 'screenshot',
56 | selector,
57 | storeBase64 = false,
58 | fullPage = false,
59 | savePng = true,
60 | } = args;
61 |
62 | console.log(`Starting screenshot with options:`, args);
63 |
64 | // Get current tab
65 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
66 | if (!tabs[0]) {
67 | return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
68 | }
69 | const tab = tabs[0];
70 |
71 | // Check URL restrictions
72 | if (
73 | tab.url?.startsWith('chrome://') ||
74 | tab.url?.startsWith('edge://') ||
75 | tab.url?.startsWith('https://chrome.google.com/webstore') ||
76 | tab.url?.startsWith('https://microsoftedge.microsoft.com/')
77 | ) {
78 | return createErrorResponse(
79 | 'Cannot capture special browser pages or web store pages due to security restrictions.',
80 | );
81 | }
82 |
83 | let finalImageDataUrl: string | undefined;
84 | const results: any = { base64: null, fileSaved: false };
85 | let originalScroll = { x: 0, y: 0 };
86 |
87 | try {
88 | await this.injectContentScript(tab.id!, ['inject-scripts/screenshot-helper.js']);
89 | // Wait for script initialization
90 | await new Promise((resolve) => setTimeout(resolve, SCREENSHOT_CONSTANTS.SCRIPT_INIT_DELAY));
91 | // 1. Prepare page (hide scrollbars, potentially fixed elements)
92 | await this.sendMessageToTab(tab.id!, {
93 | action: TOOL_MESSAGE_TYPES.SCREENSHOT_PREPARE_PAGE_FOR_CAPTURE,
94 | options: { fullPage },
95 | });
96 |
97 | // Get initial page details, including original scroll position
98 | const pageDetails = await this.sendMessageToTab(tab.id!, {
99 | action: TOOL_MESSAGE_TYPES.SCREENSHOT_GET_PAGE_DETAILS,
100 | });
101 | originalScroll = { x: pageDetails.currentScrollX, y: pageDetails.currentScrollY };
102 |
103 | if (fullPage) {
104 | this.logInfo('Capturing full page...');
105 | finalImageDataUrl = await this._captureFullPage(tab.id!, args, pageDetails);
106 | } else if (selector) {
107 | this.logInfo(`Capturing element: ${selector}`);
108 | finalImageDataUrl = await this._captureElement(tab.id!, args, pageDetails.devicePixelRatio);
109 | } else {
110 | // Visible area only
111 | this.logInfo('Capturing visible area...');
112 | finalImageDataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
113 | }
114 |
115 | if (!finalImageDataUrl) {
116 | throw new Error('Failed to capture image data');
117 | }
118 |
119 | // 2. Process output
120 | if (storeBase64 === true) {
121 | // Compress image for base64 output to reduce size
122 | const compressed = await compressImage(finalImageDataUrl, {
123 | scale: 0.7, // Reduce dimensions by 30%
124 | quality: 0.8, // 80% quality for good balance
125 | format: 'image/jpeg', // JPEG for better compression
126 | });
127 |
128 | // Include base64 data in response (without prefix)
129 | const base64Data = compressed.dataUrl.replace(/^data:image\/[^;]+;base64,/, '');
130 | results.base64 = base64Data;
131 | return {
132 | content: [
133 | {
134 | type: 'text',
135 | text: JSON.stringify({ base64Data, mimeType: compressed.mimeType }),
136 | },
137 | ],
138 | isError: false,
139 | };
140 | }
141 |
142 | if (savePng === true) {
143 | // Save PNG file to downloads
144 | this.logInfo('Saving PNG...');
145 | try {
146 | // Generate filename
147 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
148 | const filename = `${name.replace(/[^a-z0-9_-]/gi, '_') || 'screenshot'}_${timestamp}.png`;
149 |
150 | // Use Chrome's download API to save the file
151 | const downloadId = await chrome.downloads.download({
152 | url: finalImageDataUrl,
153 | filename: filename,
154 | saveAs: false,
155 | });
156 |
157 | results.downloadId = downloadId;
158 | results.filename = filename;
159 | results.fileSaved = true;
160 |
161 | // Try to get the full file path
162 | try {
163 | // Wait a moment to ensure download info is updated
164 | await new Promise((resolve) => setTimeout(resolve, 100));
165 |
166 | // Search for download item to get full path
167 | const [downloadItem] = await chrome.downloads.search({ id: downloadId });
168 | if (downloadItem && downloadItem.filename) {
169 | // Add full path to response
170 | results.fullPath = downloadItem.filename;
171 | }
172 | } catch (pathError) {
173 | console.warn('Could not get full file path:', pathError);
174 | }
175 | } catch (error) {
176 | console.error('Error saving PNG file:', error);
177 | results.saveError = String(error instanceof Error ? error.message : error);
178 | }
179 | }
180 | } catch (error) {
181 | console.error('Error during screenshot execution:', error);
182 | return createErrorResponse(
183 | `Screenshot error: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
184 | );
185 | } finally {
186 | // 3. Reset page
187 | try {
188 | await this.sendMessageToTab(tab.id!, {
189 | action: TOOL_MESSAGE_TYPES.SCREENSHOT_RESET_PAGE_AFTER_CAPTURE,
190 | scrollX: originalScroll.x,
191 | scrollY: originalScroll.y,
192 | });
193 | } catch (err) {
194 | console.warn('Failed to reset page, tab might have closed:', err);
195 | }
196 | }
197 |
198 | this.logInfo('Screenshot completed!');
199 |
200 | return {
201 | content: [
202 | {
203 | type: 'text',
204 | text: JSON.stringify({
205 | success: true,
206 | message: `Screenshot [${name}] captured successfully`,
207 | tabId: tab.id,
208 | url: tab.url,
209 | name: name,
210 | ...results,
211 | }),
212 | },
213 | ],
214 | isError: false,
215 | };
216 | }
217 |
218 | /**
219 | * Log information
220 | */
221 | private logInfo(message: string) {
222 | console.log(`[Screenshot Tool] ${message}`);
223 | }
224 |
225 | /**
226 | * Capture specific element
227 | */
228 | async _captureElement(
229 | tabId: number,
230 | options: ScreenshotToolParams,
231 | pageDpr: number,
232 | ): Promise<string> {
233 | const elementDetails = await this.sendMessageToTab(tabId, {
234 | action: TOOL_MESSAGE_TYPES.SCREENSHOT_GET_ELEMENT_DETAILS,
235 | selector: options.selector,
236 | });
237 |
238 | const dpr = elementDetails.devicePixelRatio || pageDpr || 1;
239 |
240 | // Element rect is viewport-relative, in CSS pixels
241 | // captureVisibleTab captures in physical pixels
242 | const cropRectPx = {
243 | x: elementDetails.rect.x * dpr,
244 | y: elementDetails.rect.y * dpr,
245 | width: elementDetails.rect.width * dpr,
246 | height: elementDetails.rect.height * dpr,
247 | };
248 |
249 | // Small delay to ensure element is fully rendered after scrollIntoView
250 | await new Promise((resolve) => setTimeout(resolve, SCREENSHOT_CONSTANTS.SCRIPT_INIT_DELAY));
251 |
252 | const visibleCaptureDataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
253 | if (!visibleCaptureDataUrl) {
254 | throw new Error('Failed to capture visible tab for element cropping');
255 | }
256 |
257 | const croppedCanvas = await cropAndResizeImage(
258 | visibleCaptureDataUrl,
259 | cropRectPx,
260 | dpr,
261 | options.width, // Target output width in CSS pixels
262 | options.height, // Target output height in CSS pixels
263 | );
264 | return canvasToDataURL(croppedCanvas);
265 | }
266 |
267 | /**
268 | * Capture full page
269 | */
270 | async _captureFullPage(
271 | tabId: number,
272 | options: ScreenshotToolParams,
273 | initialPageDetails: any,
274 | ): Promise<string> {
275 | const dpr = initialPageDetails.devicePixelRatio;
276 | const totalWidthCss = options.width || initialPageDetails.totalWidth; // Use option width if provided
277 | const totalHeightCss = initialPageDetails.totalHeight; // Full page always uses actual height
278 |
279 | // Apply maximum height limit for infinite scroll pages
280 | const maxHeightPx = options.maxHeight || SCREENSHOT_CONSTANTS.MAX_CAPTURE_HEIGHT_PX;
281 | const limitedHeightCss = Math.min(totalHeightCss, maxHeightPx / dpr);
282 |
283 | const totalWidthPx = totalWidthCss * dpr;
284 | const totalHeightPx = limitedHeightCss * dpr;
285 |
286 | // Viewport dimensions (CSS pixels) - logged for debugging
287 | this.logInfo(
288 | `Viewport size: ${initialPageDetails.viewportWidth}x${initialPageDetails.viewportHeight} CSS pixels`,
289 | );
290 | this.logInfo(
291 | `Page dimensions: ${totalWidthCss}x${totalHeightCss} CSS pixels (limited to ${limitedHeightCss} height)`,
292 | );
293 |
294 | const viewportHeightCss = initialPageDetails.viewportHeight;
295 |
296 | const capturedParts = [];
297 | let currentScrollYCss = 0;
298 | let capturedHeightPx = 0;
299 | let partIndex = 0;
300 |
301 | while (capturedHeightPx < totalHeightPx && partIndex < SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS) {
302 | this.logInfo(
303 | `Capturing part ${partIndex + 1}... (${Math.round((capturedHeightPx / totalHeightPx) * 100)}%)`,
304 | );
305 |
306 | if (currentScrollYCss > 0) {
307 | // Don't scroll for the first part if already at top
308 | const scrollResp = await this.sendMessageToTab(tabId, {
309 | action: TOOL_MESSAGE_TYPES.SCREENSHOT_SCROLL_PAGE,
310 | x: 0,
311 | y: currentScrollYCss,
312 | scrollDelay: SCREENSHOT_CONSTANTS.SCROLL_DELAY_MS,
313 | });
314 | // Update currentScrollYCss based on actual scroll achieved
315 | currentScrollYCss = scrollResp.newScrollY;
316 | }
317 |
318 | // Ensure rendering after scroll
319 | await new Promise((resolve) =>
320 | setTimeout(resolve, SCREENSHOT_CONSTANTS.CAPTURE_STITCH_DELAY_MS),
321 | );
322 |
323 | const dataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
324 | if (!dataUrl) throw new Error('captureVisibleTab returned empty during full page capture');
325 |
326 | const yOffsetPx = currentScrollYCss * dpr;
327 | capturedParts.push({ dataUrl, y: yOffsetPx });
328 |
329 | const imgForHeight = await createImageBitmapFromUrl(dataUrl); // To get actual captured height
330 | const lastPartEffectiveHeightPx = Math.min(imgForHeight.height, totalHeightPx - yOffsetPx);
331 |
332 | capturedHeightPx = yOffsetPx + lastPartEffectiveHeightPx;
333 |
334 | if (capturedHeightPx >= totalHeightPx - SCREENSHOT_CONSTANTS.PIXEL_TOLERANCE) break;
335 |
336 | currentScrollYCss += viewportHeightCss;
337 | // Prevent overscrolling past the document height for the next scroll command
338 | if (
339 | currentScrollYCss > totalHeightCss - viewportHeightCss &&
340 | currentScrollYCss < totalHeightCss
341 | ) {
342 | currentScrollYCss = totalHeightCss - viewportHeightCss;
343 | }
344 | partIndex++;
345 | }
346 |
347 | // Check if we hit any limits
348 | if (partIndex >= SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS) {
349 | this.logInfo(
350 | `Reached maximum number of capture parts (${SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS}). This may be an infinite scroll page.`,
351 | );
352 | }
353 | if (totalHeightCss > limitedHeightCss) {
354 | this.logInfo(
355 | `Page height (${totalHeightCss}px) exceeds maximum capture height (${maxHeightPx / dpr}px). Capturing limited portion.`,
356 | );
357 | }
358 |
359 | this.logInfo('Stitching image...');
360 | const finalCanvas = await stitchImages(capturedParts, totalWidthPx, totalHeightPx);
361 |
362 | // If user specified width but not height (or vice versa for full page), resize maintaining aspect ratio
363 | let outputCanvas = finalCanvas;
364 | if (options.width && !options.height) {
365 | const targetWidthPx = options.width * dpr;
366 | const aspectRatio = finalCanvas.height / finalCanvas.width;
367 | const targetHeightPx = targetWidthPx * aspectRatio;
368 | outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
369 | const ctx = outputCanvas.getContext('2d');
370 | if (ctx) {
371 | ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
372 | }
373 | } else if (options.height && !options.width) {
374 | const targetHeightPx = options.height * dpr;
375 | const aspectRatio = finalCanvas.width / finalCanvas.height;
376 | const targetWidthPx = targetHeightPx * aspectRatio;
377 | outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
378 | const ctx = outputCanvas.getContext('2d');
379 | if (ctx) {
380 | ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
381 | }
382 | } else if (options.width && options.height) {
383 | // Both specified, direct resize
384 | const targetWidthPx = options.width * dpr;
385 | const targetHeightPx = options.height * dpr;
386 | outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
387 | const ctx = outputCanvas.getContext('2d');
388 | if (ctx) {
389 | ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
390 | }
391 | }
392 |
393 | return canvasToDataURL(outputCanvas);
394 | }
395 | }
396 |
397 | export const screenshotTool = new ScreenshotTool();
398 |
```