chrome-devtools-mcp-for-extension 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/McpContext.js +24 -3
- package/build/src/browser-connection-manager.js +198 -0
- package/build/src/main.js +28 -3
- package/build/src/selectors/chatgpt.json +85 -0
- package/build/src/selectors/loader.js +102 -0
- package/build/src/startup-check.js +142 -0
- package/build/src/tools/chatgpt-web.js +23 -12
- package/build/src/tools/diagnose-ui.js +310 -0
- package/package.json +2 -1
package/build/src/McpContext.js
CHANGED
|
@@ -10,6 +10,7 @@ import { NetworkCollector, PageCollector } from './PageCollector.js';
|
|
|
10
10
|
import { listPages } from './tools/pages.js';
|
|
11
11
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
12
12
|
import { WaitForHelper } from './WaitForHelper.js';
|
|
13
|
+
import { BrowserConnectionManager, } from './browser-connection-manager.js';
|
|
13
14
|
const DEFAULT_TIMEOUT = 5_000;
|
|
14
15
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
15
16
|
function getNetworkMultiplierFromString(condition) {
|
|
@@ -29,6 +30,7 @@ function getNetworkMultiplierFromString(condition) {
|
|
|
29
30
|
export class McpContext {
|
|
30
31
|
browser;
|
|
31
32
|
logger;
|
|
33
|
+
connectionManager;
|
|
32
34
|
// The most recent page state.
|
|
33
35
|
#pages = [];
|
|
34
36
|
#selectedPageIdx = 0;
|
|
@@ -42,9 +44,14 @@ export class McpContext {
|
|
|
42
44
|
#dialog;
|
|
43
45
|
#nextSnapshotId = 1;
|
|
44
46
|
#traceResults = [];
|
|
45
|
-
constructor(browser, logger) {
|
|
47
|
+
constructor(browser, logger, browserFactory, connectionOptions) {
|
|
46
48
|
this.browser = browser;
|
|
47
49
|
this.logger = logger;
|
|
50
|
+
this.connectionManager = new BrowserConnectionManager(connectionOptions);
|
|
51
|
+
// Set up browser instance and factory for reconnection
|
|
52
|
+
if (browserFactory) {
|
|
53
|
+
this.connectionManager.setBrowser(browser, browserFactory);
|
|
54
|
+
}
|
|
48
55
|
this.#networkCollector = new NetworkCollector(this.browser, (page, collect) => {
|
|
49
56
|
page.on('request', request => {
|
|
50
57
|
collect(request);
|
|
@@ -65,11 +72,25 @@ export class McpContext {
|
|
|
65
72
|
await this.#networkCollector.init();
|
|
66
73
|
await this.#consoleCollector.init();
|
|
67
74
|
}
|
|
68
|
-
static async from(browser, logger) {
|
|
69
|
-
const context = new McpContext(browser, logger);
|
|
75
|
+
static async from(browser, logger, browserFactory, connectionOptions) {
|
|
76
|
+
const context = new McpContext(browser, logger, browserFactory, connectionOptions);
|
|
70
77
|
await context.#init();
|
|
71
78
|
return context;
|
|
72
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Update browser instance after reconnection
|
|
82
|
+
*/
|
|
83
|
+
async updateBrowser(newBrowser) {
|
|
84
|
+
this.browser = newBrowser;
|
|
85
|
+
this.connectionManager.setBrowser(newBrowser, async () => newBrowser);
|
|
86
|
+
// Reinitialize collectors for new browser
|
|
87
|
+
await this.#networkCollector.init();
|
|
88
|
+
await this.#consoleCollector.init();
|
|
89
|
+
// Recreate pages snapshot
|
|
90
|
+
await this.createPagesSnapshot();
|
|
91
|
+
this.setSelectedPageIdx(0);
|
|
92
|
+
this.logger('Browser instance updated after reconnection');
|
|
93
|
+
}
|
|
73
94
|
getNetworkRequests() {
|
|
74
95
|
const page = this.getSelectedPage();
|
|
75
96
|
return this.#networkCollector.getData(page);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Default connection manager options
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
maxReconnectAttempts: 3,
|
|
11
|
+
initialRetryDelay: 1000, // 1 second
|
|
12
|
+
maxRetryDelay: 10000, // 10 seconds
|
|
13
|
+
enableLogging: true,
|
|
14
|
+
onReconnect: undefined,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Browser Connection Manager
|
|
18
|
+
*
|
|
19
|
+
* Provides automatic reconnection for CDP operations
|
|
20
|
+
*/
|
|
21
|
+
export class BrowserConnectionManager {
|
|
22
|
+
reconnectAttempts = 0;
|
|
23
|
+
options;
|
|
24
|
+
browser = null;
|
|
25
|
+
browserFactory = null;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set browser instance and factory for reconnection
|
|
31
|
+
*/
|
|
32
|
+
setBrowser(browser, factory) {
|
|
33
|
+
this.browser = browser;
|
|
34
|
+
this.browserFactory = factory;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Execute an operation with automatic retry on CDP connection errors
|
|
38
|
+
*/
|
|
39
|
+
async executeWithRetry(operation, operationName) {
|
|
40
|
+
try {
|
|
41
|
+
return await operation();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (this.isCDPConnectionError(error)) {
|
|
45
|
+
this.log(`CDP connection error in ${operationName}, attempting reconnect...`);
|
|
46
|
+
return await this.retryWithReconnect(operation, operationName);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Retry operation with exponential backoff
|
|
53
|
+
*/
|
|
54
|
+
async retryWithReconnect(operation, operationName) {
|
|
55
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? 3;
|
|
56
|
+
const initialDelay = this.options.initialRetryDelay ?? 1000;
|
|
57
|
+
const maxDelay = this.options.maxRetryDelay ?? 10000;
|
|
58
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
59
|
+
this.reconnectAttempts++;
|
|
60
|
+
const attemptNum = attempt + 1;
|
|
61
|
+
this.log(`Reconnect attempt ${attemptNum}/${maxAttempts} for ${operationName}...`);
|
|
62
|
+
// Exponential backoff with max delay
|
|
63
|
+
const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
|
|
64
|
+
await this.sleep(delay);
|
|
65
|
+
try {
|
|
66
|
+
await this.reconnectBrowser();
|
|
67
|
+
this.log(`Reconnection successful, retrying ${operationName}...`);
|
|
68
|
+
return await operation();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (attempt === maxAttempts - 1) {
|
|
72
|
+
// Last attempt failed
|
|
73
|
+
throw this.createReconnectionFailedError(attemptNum, error);
|
|
74
|
+
}
|
|
75
|
+
this.log(`Reconnect attempt ${attemptNum} failed: ${error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Reconnection logic error'); // Should never reach here
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if error is a CDP connection error
|
|
82
|
+
*/
|
|
83
|
+
isCDPConnectionError(error) {
|
|
84
|
+
const errorMessage = error?.message || '';
|
|
85
|
+
return (errorMessage.includes('Target closed') ||
|
|
86
|
+
errorMessage.includes('Protocol error') ||
|
|
87
|
+
errorMessage.includes('Session closed') ||
|
|
88
|
+
errorMessage.includes('Connection closed') ||
|
|
89
|
+
errorMessage.includes('WebSocket is not open'));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Reconnect browser instance
|
|
93
|
+
*/
|
|
94
|
+
async reconnectBrowser() {
|
|
95
|
+
if (!this.browserFactory) {
|
|
96
|
+
throw new Error('Browser factory not set. Cannot reconnect.');
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
// Close old browser if still connected
|
|
100
|
+
if (this.browser?.isConnected()) {
|
|
101
|
+
await this.browser.close().catch(() => {
|
|
102
|
+
// Ignore errors during close
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Ignore errors during cleanup
|
|
108
|
+
}
|
|
109
|
+
// Create new browser instance
|
|
110
|
+
const newBrowser = await this.browserFactory();
|
|
111
|
+
this.browser = newBrowser;
|
|
112
|
+
this.log('Browser reconnected successfully');
|
|
113
|
+
// Notify callback if provided
|
|
114
|
+
if (this.options.onReconnect) {
|
|
115
|
+
await this.options.onReconnect(newBrowser);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Create user-friendly error for reconnection failure
|
|
120
|
+
*/
|
|
121
|
+
createReconnectionFailedError(attempts, lastError) {
|
|
122
|
+
const message = `
|
|
123
|
+
❌ Chrome DevTools接続エラー
|
|
124
|
+
|
|
125
|
+
${attempts}回の再接続を試みましたが、Chrome DevToolsとの接続を回復できませんでした。
|
|
126
|
+
|
|
127
|
+
📋 最後のエラー:
|
|
128
|
+
${lastError?.message || 'Unknown error'}
|
|
129
|
+
|
|
130
|
+
🔧 解決方法:
|
|
131
|
+
1. Claude Codeを再起動してください
|
|
132
|
+
2. Chromeブラウザを完全に終了して再起動してください
|
|
133
|
+
3. chrome://extensions でChrome DevTools拡張機能を確認してください
|
|
134
|
+
|
|
135
|
+
詳細: docs/troubleshooting.md#cdp-connection-error
|
|
136
|
+
`.trim();
|
|
137
|
+
const error = new Error(message);
|
|
138
|
+
error.name = 'CDPReconnectionError';
|
|
139
|
+
return error;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Log message if logging is enabled
|
|
143
|
+
*/
|
|
144
|
+
log(message) {
|
|
145
|
+
if (this.options.enableLogging !== false) {
|
|
146
|
+
console.log(`[ConnectionManager] ${message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sleep for specified milliseconds
|
|
151
|
+
*/
|
|
152
|
+
sleep(ms) {
|
|
153
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get current browser instance
|
|
157
|
+
*/
|
|
158
|
+
getBrowser() {
|
|
159
|
+
return this.browser;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get total reconnection attempts made
|
|
163
|
+
*/
|
|
164
|
+
getReconnectAttempts() {
|
|
165
|
+
return this.reconnectAttempts;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Reset reconnection attempt counter
|
|
169
|
+
*/
|
|
170
|
+
resetReconnectAttempts() {
|
|
171
|
+
this.reconnectAttempts = 0;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if browser is currently connected
|
|
175
|
+
*/
|
|
176
|
+
isConnected() {
|
|
177
|
+
return this.browser?.isConnected() ?? false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Global connection manager instance
|
|
182
|
+
*/
|
|
183
|
+
let globalConnectionManager = null;
|
|
184
|
+
/**
|
|
185
|
+
* Get or create global connection manager
|
|
186
|
+
*/
|
|
187
|
+
export function getConnectionManager(options) {
|
|
188
|
+
if (!globalConnectionManager) {
|
|
189
|
+
globalConnectionManager = new BrowserConnectionManager(options);
|
|
190
|
+
}
|
|
191
|
+
return globalConnectionManager;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Reset global connection manager (for testing)
|
|
195
|
+
*/
|
|
196
|
+
export function resetConnectionManager() {
|
|
197
|
+
globalConnectionManager = null;
|
|
198
|
+
}
|
package/build/src/main.js
CHANGED
|
@@ -15,10 +15,12 @@ import { logger, saveLogsToFile } from './logger.js';
|
|
|
15
15
|
import { McpContext } from './McpContext.js';
|
|
16
16
|
import { McpResponse } from './McpResponse.js';
|
|
17
17
|
import { Mutex } from './Mutex.js';
|
|
18
|
+
import { runStartupCheck } from './startup-check.js';
|
|
18
19
|
import * as bookmarkTools from './tools/bookmarks.js';
|
|
19
20
|
import * as chatgptWebTools from './tools/chatgpt-web.js';
|
|
20
21
|
import * as deepResearchChatGPTTools from './tools/deep_research_chatgpt.js';
|
|
21
22
|
import * as consoleTools from './tools/console.js';
|
|
23
|
+
import * as diagnoseUiTools from './tools/diagnose-ui.js';
|
|
22
24
|
import * as emulationTools from './tools/emulation.js';
|
|
23
25
|
import * as extensionTools from './tools/extensions.js';
|
|
24
26
|
import * as inputTools from './tools/input.js';
|
|
@@ -56,8 +58,9 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
|
|
|
56
58
|
return {};
|
|
57
59
|
});
|
|
58
60
|
let context;
|
|
61
|
+
let uiHealthCheckRun = false; // Track if UI health check has been run
|
|
59
62
|
async function getContext() {
|
|
60
|
-
const
|
|
63
|
+
const browserOptions = {
|
|
61
64
|
browserUrl: args.browserUrl,
|
|
62
65
|
headless: args.headless,
|
|
63
66
|
executablePath: args.executablePath,
|
|
@@ -70,10 +73,31 @@ async function getContext() {
|
|
|
70
73
|
chromeProfile: args.chromeProfile,
|
|
71
74
|
userDataDir: args.userDataDir,
|
|
72
75
|
logFile,
|
|
73
|
-
}
|
|
76
|
+
};
|
|
77
|
+
const browser = await resolveBrowser(browserOptions);
|
|
78
|
+
// Browser factory function for reconnection
|
|
79
|
+
const browserFactory = async () => {
|
|
80
|
+
logger('Reconnecting browser...');
|
|
81
|
+
return await resolveBrowser(browserOptions);
|
|
82
|
+
};
|
|
74
83
|
// Always recreate context if browser reference changed or context doesn't exist
|
|
75
84
|
if (!context || context.browser !== browser) {
|
|
76
|
-
|
|
85
|
+
// Connection manager options with reconnect callback
|
|
86
|
+
const connectionOptions = {
|
|
87
|
+
onReconnect: async (newBrowser) => {
|
|
88
|
+
logger('Updating context with reconnected browser...');
|
|
89
|
+
await context.updateBrowser(newBrowser);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
context = await McpContext.from(browser, logger, browserFactory, connectionOptions);
|
|
93
|
+
// Run UI health check only once per browser instance
|
|
94
|
+
if (!uiHealthCheckRun) {
|
|
95
|
+
uiHealthCheckRun = true;
|
|
96
|
+
// Run in background to avoid blocking startup
|
|
97
|
+
runStartupCheck(browser).catch(err => {
|
|
98
|
+
logger(`Startup check failed: ${err}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
77
101
|
}
|
|
78
102
|
return context;
|
|
79
103
|
}
|
|
@@ -138,6 +162,7 @@ const tools = [
|
|
|
138
162
|
...Object.values(chatgptWebTools),
|
|
139
163
|
...Object.values(deepResearchChatGPTTools),
|
|
140
164
|
...Object.values(consoleTools),
|
|
165
|
+
...Object.values(diagnoseUiTools),
|
|
141
166
|
...Object.values(emulationTools),
|
|
142
167
|
...Object.values(extensionTools),
|
|
143
168
|
...Object.values(inputTools),
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2025-10",
|
|
3
|
+
"lastUpdated": "2025-10-03",
|
|
4
|
+
"description": "ChatGPT Web UI selectors for browser automation",
|
|
5
|
+
"elements": {
|
|
6
|
+
"newChatLink": {
|
|
7
|
+
"css": "a[href='/']",
|
|
8
|
+
"description": "新しいチャット作成リンク"
|
|
9
|
+
},
|
|
10
|
+
"tempChatToggle": {
|
|
11
|
+
"ax": {
|
|
12
|
+
"role": "button",
|
|
13
|
+
"nameContains": "一時チャットをオフにする"
|
|
14
|
+
},
|
|
15
|
+
"description": "一時チャットON/OFFトグルボタン"
|
|
16
|
+
},
|
|
17
|
+
"addFileButton": {
|
|
18
|
+
"ax": {
|
|
19
|
+
"role": "button",
|
|
20
|
+
"ariaLabel": "ファイルの追加"
|
|
21
|
+
},
|
|
22
|
+
"description": "ファイル追加(ツールメニューを開く)ボタン"
|
|
23
|
+
},
|
|
24
|
+
"deepResearchMenuItem": {
|
|
25
|
+
"css": "[role='menuitemradio']",
|
|
26
|
+
"text": "Deep Research",
|
|
27
|
+
"description": "DeepResearchモード切り替えメニュー項目"
|
|
28
|
+
},
|
|
29
|
+
"deepResearchDeleteButton": {
|
|
30
|
+
"text": "リサーチ:クリックして削除",
|
|
31
|
+
"description": "DeepResearch削除ボタン(モード確認用)"
|
|
32
|
+
},
|
|
33
|
+
"deepResearchSourcesButton": {
|
|
34
|
+
"text": "情報源",
|
|
35
|
+
"description": "情報源ボタン(モード確認用)"
|
|
36
|
+
},
|
|
37
|
+
"composerTextarea": {
|
|
38
|
+
"css": "textarea[placeholder]",
|
|
39
|
+
"description": "メッセージ入力テキストエリア"
|
|
40
|
+
},
|
|
41
|
+
"prosemirrorEditor": {
|
|
42
|
+
"css": ".ProseMirror[contenteditable='true']",
|
|
43
|
+
"description": "ProseMirrorエディタ"
|
|
44
|
+
},
|
|
45
|
+
"sendButton": {
|
|
46
|
+
"css": "button[data-testid='send-button']",
|
|
47
|
+
"description": "送信ボタン"
|
|
48
|
+
},
|
|
49
|
+
"userMessage": {
|
|
50
|
+
"css": "[data-message-author-role='user']",
|
|
51
|
+
"description": "ユーザーメッセージ"
|
|
52
|
+
},
|
|
53
|
+
"assistantMessage": {
|
|
54
|
+
"css": "[data-message-author-role='assistant']",
|
|
55
|
+
"description": "アシスタントメッセージ"
|
|
56
|
+
},
|
|
57
|
+
"stopButton": {
|
|
58
|
+
"text": [
|
|
59
|
+
"停止",
|
|
60
|
+
"リサーチを停止",
|
|
61
|
+
"ストリーミングの停止"
|
|
62
|
+
],
|
|
63
|
+
"ax": {
|
|
64
|
+
"ariaLabel": [
|
|
65
|
+
"停止",
|
|
66
|
+
"ストリーミングの停止"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"description": "ストリーミング停止ボタン"
|
|
70
|
+
},
|
|
71
|
+
"thinkingTimeButton": {
|
|
72
|
+
"ax": {
|
|
73
|
+
"ariaLabelContains": "思考時間"
|
|
74
|
+
},
|
|
75
|
+
"description": "思考時間表示ボタン"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"placeholders": {
|
|
79
|
+
"normalMode": "Message ChatGPT",
|
|
80
|
+
"deepResearchMode": [
|
|
81
|
+
"詳細なレポート",
|
|
82
|
+
"リサーチ"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
let cachedSelectors = null;
|
|
10
|
+
/**
|
|
11
|
+
* Load ChatGPT selectors from JSON file
|
|
12
|
+
*/
|
|
13
|
+
export function loadSelectors() {
|
|
14
|
+
if (cachedSelectors) {
|
|
15
|
+
return cachedSelectors;
|
|
16
|
+
}
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const selectorsPath = path.join(__dirname, 'chatgpt.json');
|
|
20
|
+
try {
|
|
21
|
+
const data = fs.readFileSync(selectorsPath, 'utf-8');
|
|
22
|
+
cachedSelectors = JSON.parse(data);
|
|
23
|
+
return cachedSelectors;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new Error(`Failed to load selectors from ${selectorsPath}: ${error}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get a specific selector definition
|
|
31
|
+
*/
|
|
32
|
+
export function getSelector(elementName) {
|
|
33
|
+
const selectors = loadSelectors();
|
|
34
|
+
const selector = selectors.elements[elementName];
|
|
35
|
+
if (!selector) {
|
|
36
|
+
throw new Error(`Selector not found: ${elementName}`);
|
|
37
|
+
}
|
|
38
|
+
return selector;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate page.evaluate() compatible selector function for text matching
|
|
42
|
+
*/
|
|
43
|
+
export function generateTextMatcher(selector, tagName = 'button') {
|
|
44
|
+
const textPatterns = Array.isArray(selector.text)
|
|
45
|
+
? selector.text
|
|
46
|
+
: [selector.text];
|
|
47
|
+
return `
|
|
48
|
+
Array.from(document.querySelectorAll('${tagName}')).find(el => {
|
|
49
|
+
const text = el.textContent || '';
|
|
50
|
+
return ${textPatterns.map((t) => `text.includes('${t}')`).join(' || ')};
|
|
51
|
+
})
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate page.evaluate() compatible selector function for accessibility matching
|
|
56
|
+
*/
|
|
57
|
+
export function generateAxMatcher(selector, tagName = 'button') {
|
|
58
|
+
if (!selector.ax) {
|
|
59
|
+
throw new Error('Accessibility selector not defined');
|
|
60
|
+
}
|
|
61
|
+
const conditions = [];
|
|
62
|
+
if (selector.ax.ariaLabel) {
|
|
63
|
+
const labels = Array.isArray(selector.ax.ariaLabel)
|
|
64
|
+
? selector.ax.ariaLabel
|
|
65
|
+
: [selector.ax.ariaLabel];
|
|
66
|
+
conditions.push(labels.map((label) => `aria.includes('${label}')`).join(' || '));
|
|
67
|
+
}
|
|
68
|
+
if (selector.ax.ariaLabelContains) {
|
|
69
|
+
conditions.push(`aria.includes('${selector.ax.ariaLabelContains}')`);
|
|
70
|
+
}
|
|
71
|
+
if (selector.ax.nameContains) {
|
|
72
|
+
conditions.push(`aria.includes('${selector.ax.nameContains}')`);
|
|
73
|
+
}
|
|
74
|
+
return `
|
|
75
|
+
Array.from(document.querySelectorAll('${tagName}')).find(el => {
|
|
76
|
+
const aria = el.getAttribute('aria-label') || '';
|
|
77
|
+
return ${conditions.join(' || ')};
|
|
78
|
+
})
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if placeholder matches expected patterns
|
|
83
|
+
*/
|
|
84
|
+
export function checkPlaceholder(placeholder, mode) {
|
|
85
|
+
const selectors = loadSelectors();
|
|
86
|
+
if (mode === 'normal' && selectors.placeholders?.normalMode) {
|
|
87
|
+
return placeholder.includes(selectors.placeholders.normalMode);
|
|
88
|
+
}
|
|
89
|
+
if (mode === 'deepResearch' && selectors.placeholders?.deepResearchMode) {
|
|
90
|
+
const patterns = Array.isArray(selectors.placeholders.deepResearchMode)
|
|
91
|
+
? selectors.placeholders.deepResearchMode
|
|
92
|
+
: [selectors.placeholders.deepResearchMode];
|
|
93
|
+
return patterns.some((pattern) => placeholder.includes(pattern));
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Clear cached selectors (useful for testing or hot-reload)
|
|
99
|
+
*/
|
|
100
|
+
export function clearCache() {
|
|
101
|
+
cachedSelectors = null;
|
|
102
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* UI elements to check during health verification
|
|
8
|
+
*/
|
|
9
|
+
const UI_ELEMENTS = [
|
|
10
|
+
{
|
|
11
|
+
name: 'Deep Research Toggle',
|
|
12
|
+
selector: '[role="menuitemradio"]', // Deep Research menu item
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'Composer Textarea',
|
|
16
|
+
selector: 'textarea, .ProseMirror[contenteditable="true"]', // ChatGPT input area
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Send Button',
|
|
20
|
+
selector: 'button[data-testid="send-button"]', // Send message button
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Check if UI element exists using accessibility tree
|
|
25
|
+
*/
|
|
26
|
+
async function checkElementExists(page, element) {
|
|
27
|
+
try {
|
|
28
|
+
// First try direct DOM query
|
|
29
|
+
const domCheck = await page.evaluate((selector) => {
|
|
30
|
+
return document.querySelector(selector) !== null;
|
|
31
|
+
}, element.selector);
|
|
32
|
+
if (domCheck) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Fallback: Check accessibility tree
|
|
36
|
+
const axTree = await page.accessibility.snapshot();
|
|
37
|
+
if (!axTree) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Search for element by name in AX tree
|
|
41
|
+
function searchAxTree(node) {
|
|
42
|
+
if (node.name?.includes(element.name)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (node.children) {
|
|
46
|
+
for (const child of node.children) {
|
|
47
|
+
if (searchAxTree(child)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return searchAxTree(axTree);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error(`Error checking ${element.name}:`, error);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify ChatGPT UI health on startup
|
|
63
|
+
*/
|
|
64
|
+
export async function verifyUIHealth(browser) {
|
|
65
|
+
console.error('\n🔍 UI Health Check: Starting...');
|
|
66
|
+
let page;
|
|
67
|
+
try {
|
|
68
|
+
// Get or create a page
|
|
69
|
+
const pages = await browser.pages();
|
|
70
|
+
page = pages[0] || (await browser.newPage());
|
|
71
|
+
// Navigate to ChatGPT with short timeout
|
|
72
|
+
console.error(' Navigating to ChatGPT...');
|
|
73
|
+
await page.goto('https://chatgpt.com/', {
|
|
74
|
+
waitUntil: 'networkidle2',
|
|
75
|
+
timeout: 30000,
|
|
76
|
+
});
|
|
77
|
+
// Wait for page to be ready
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
79
|
+
// Check login status
|
|
80
|
+
const currentUrl = page.url();
|
|
81
|
+
if (currentUrl.includes('auth') || currentUrl.includes('login')) {
|
|
82
|
+
console.error('⚠️ Not logged in to ChatGPT - skipping UI verification');
|
|
83
|
+
console.error(' Please log in manually for full functionality');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Check each UI element
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const element of UI_ELEMENTS) {
|
|
89
|
+
const found = await checkElementExists(page, element);
|
|
90
|
+
results.push({ element, found });
|
|
91
|
+
if (found) {
|
|
92
|
+
console.error(`✅ ${element.name}: Found`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const status = element.optional ? '⚠️' : '❌';
|
|
96
|
+
console.error(`${status} ${element.name}: NOT FOUND`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Summary
|
|
100
|
+
const missingElements = results.filter((r) => !r.found && !r.element.optional);
|
|
101
|
+
if (missingElements.length > 0) {
|
|
102
|
+
console.error('\n⚠️ UI Health Check Warning:');
|
|
103
|
+
console.error(' Some UI elements were not found. ChatGPT UI may have changed.');
|
|
104
|
+
console.error(' Missing elements:');
|
|
105
|
+
missingElements.forEach((r) => {
|
|
106
|
+
console.error(` - ${r.element.name}`);
|
|
107
|
+
});
|
|
108
|
+
console.error('\n💡 Suggestion: Run diagnostics to investigate:');
|
|
109
|
+
console.error(' npm run diagnose:ui');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.error('\n✅ UI Health Check: All elements found');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
117
|
+
console.error(`\n❌ UI Health Check Failed: ${errorMessage}`);
|
|
118
|
+
console.error(' This is a warning only - MCP server will continue to start');
|
|
119
|
+
console.error(' ChatGPT functionality may be limited');
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
// Don't close the page - let the server reuse it
|
|
123
|
+
console.error('');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Run UI health check with timeout protection
|
|
128
|
+
*/
|
|
129
|
+
export async function runStartupCheck(browser) {
|
|
130
|
+
const timeoutMs = 35000; // 35 seconds total timeout
|
|
131
|
+
try {
|
|
132
|
+
await Promise.race([
|
|
133
|
+
verifyUIHealth(browser),
|
|
134
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('UI health check timeout')), timeoutMs)),
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
139
|
+
console.error(`⚠️ Startup check timeout or error: ${errorMessage}`);
|
|
140
|
+
console.error(' Continuing with server startup...');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import z from 'zod';
|
|
9
9
|
import { ToolCategories } from './categories.js';
|
|
10
10
|
import { defineTool } from './ToolDefinition.js';
|
|
11
|
+
import { loadSelectors, getSelector } from '../selectors/loader.js';
|
|
11
12
|
/**
|
|
12
13
|
* Path to store chat session data
|
|
13
14
|
*/
|
|
@@ -281,21 +282,26 @@ export const askChatGPTWeb = defineTool({
|
|
|
281
282
|
response.appendResponseLine('✅ DeepResearchモード有効化完了');
|
|
282
283
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
283
284
|
// Verify mode was actually enabled (check for new UI indicators)
|
|
284
|
-
const
|
|
285
|
+
const selectors = loadSelectors();
|
|
286
|
+
const verification = await page.evaluate((deleteText, sourcesText, deepResearchPlaceholders) => {
|
|
285
287
|
// Check placeholder text
|
|
286
288
|
const textarea = document.querySelector('textarea');
|
|
287
289
|
const placeholder = textarea?.getAttribute('placeholder') || '';
|
|
288
|
-
// Check for delete button
|
|
289
|
-
const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(
|
|
290
|
-
// Check for sources button
|
|
291
|
-
const sourcesButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(
|
|
290
|
+
// Check for delete button (using JSON selector)
|
|
291
|
+
const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(deleteText));
|
|
292
|
+
// Check for sources button (using JSON selector)
|
|
293
|
+
const sourcesButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(sourcesText));
|
|
294
|
+
// Check placeholder against expected patterns
|
|
295
|
+
const hasCorrectPlaceholder = deepResearchPlaceholders.some((pattern) => placeholder.includes(pattern));
|
|
292
296
|
return {
|
|
293
|
-
hasCorrectPlaceholder
|
|
297
|
+
hasCorrectPlaceholder,
|
|
294
298
|
hasDeleteButton: !!deleteButton,
|
|
295
299
|
hasSourcesButton: !!sourcesButton,
|
|
296
300
|
placeholder: placeholder
|
|
297
301
|
};
|
|
298
|
-
})
|
|
302
|
+
}, getSelector('deepResearchDeleteButton').text, getSelector('deepResearchSourcesButton').text, Array.isArray(selectors.placeholders?.deepResearchMode)
|
|
303
|
+
? selectors.placeholders.deepResearchMode
|
|
304
|
+
: [selectors.placeholders?.deepResearchMode || '']);
|
|
299
305
|
if (verification.hasCorrectPlaceholder || verification.hasDeleteButton) {
|
|
300
306
|
response.appendResponseLine('✅ モード確認完了: DeepResearch有効');
|
|
301
307
|
}
|
|
@@ -310,17 +316,22 @@ export const askChatGPTWeb = defineTool({
|
|
|
310
316
|
}
|
|
311
317
|
// Step 4: Send question (with final mode verification)
|
|
312
318
|
if (useDeepResearch) {
|
|
313
|
-
const
|
|
319
|
+
const selectorsForCheck = loadSelectors();
|
|
320
|
+
const finalCheck = await page.evaluate((deleteText, deepResearchPlaceholders) => {
|
|
314
321
|
// Check placeholder text
|
|
315
322
|
const textarea = document.querySelector('textarea');
|
|
316
323
|
const placeholder = textarea?.getAttribute('placeholder') || '';
|
|
317
|
-
// Check for delete button
|
|
318
|
-
const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(
|
|
324
|
+
// Check for delete button (using JSON selector)
|
|
325
|
+
const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(deleteText));
|
|
326
|
+
// Check if placeholder matches DeepResearch patterns
|
|
327
|
+
const placeholderMatches = deepResearchPlaceholders.some((pattern) => placeholder.includes(pattern));
|
|
319
328
|
return {
|
|
320
|
-
isEnabled:
|
|
329
|
+
isEnabled: placeholderMatches || !!deleteButton,
|
|
321
330
|
placeholder: placeholder
|
|
322
331
|
};
|
|
323
|
-
})
|
|
332
|
+
}, getSelector('deepResearchDeleteButton').text, Array.isArray(selectorsForCheck.placeholders?.deepResearchMode)
|
|
333
|
+
? selectorsForCheck.placeholders.deepResearchMode
|
|
334
|
+
: [selectorsForCheck.placeholders?.deepResearchMode || '']);
|
|
324
335
|
if (!finalCheck.isEnabled) {
|
|
325
336
|
response.appendResponseLine(`❌ エラー: DeepResearchモードが無効です。送信を中止します。(placeholder: ${finalCheck.placeholder})`);
|
|
326
337
|
return;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import z from 'zod';
|
|
9
|
+
import { ToolCategories } from './categories.js';
|
|
10
|
+
import { defineTool } from './ToolDefinition.js';
|
|
11
|
+
/**
|
|
12
|
+
* Known important elements in ChatGPT UI
|
|
13
|
+
*/
|
|
14
|
+
const KNOWN_ELEMENTS = [
|
|
15
|
+
{
|
|
16
|
+
name: 'Deep Research Toggle',
|
|
17
|
+
checks: [
|
|
18
|
+
{ type: 'css', selector: '[role="menuitemradio"][aria-label*="Deep"]' },
|
|
19
|
+
{ type: 'xpath', selector: '//div[@role="menuitemradio" and contains(text(), "Deep research")]' },
|
|
20
|
+
{ type: 'text', contains: 'Deep research' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'Composer Textarea',
|
|
25
|
+
checks: [
|
|
26
|
+
{ type: 'css', selector: 'textarea[placeholder*="Message"]' },
|
|
27
|
+
{ type: 'css', selector: '[contenteditable="true"]' },
|
|
28
|
+
{ type: 'css', selector: '#prompt-textarea' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Send Button',
|
|
33
|
+
checks: [
|
|
34
|
+
{ type: 'css', selector: 'button[data-testid="send-button"]' },
|
|
35
|
+
{ type: 'css', selector: 'button[aria-label*="Send"]' },
|
|
36
|
+
{ type: 'xpath', selector: '//button[contains(@aria-label, "Send")]' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Model Selector',
|
|
41
|
+
checks: [
|
|
42
|
+
{ type: 'css', selector: 'button[aria-label*="model"]' },
|
|
43
|
+
{ type: 'css', selector: '[role="combobox"]' },
|
|
44
|
+
{ type: 'text', contains: 'ChatGPT' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Ensure snapshot directory exists
|
|
50
|
+
*/
|
|
51
|
+
async function ensureSnapshotDir() {
|
|
52
|
+
const snapshotDir = path.join(process.cwd(), 'docs/ui-snapshots');
|
|
53
|
+
await fs.promises.mkdir(snapshotDir, { recursive: true });
|
|
54
|
+
return snapshotDir;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate timestamp-based filename
|
|
58
|
+
*/
|
|
59
|
+
function generateFilename(extension) {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const timestamp = now.toISOString()
|
|
62
|
+
.replace(/:/g, '')
|
|
63
|
+
.replace(/\..+/, '')
|
|
64
|
+
.replace('T', '-')
|
|
65
|
+
.slice(2); // Remove first 2 digits of year (20YY -> YY)
|
|
66
|
+
return `chatgpt-${timestamp}.${extension}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Detect element using multiple strategies
|
|
70
|
+
*/
|
|
71
|
+
async function detectElement(page, elementDef) {
|
|
72
|
+
const result = {
|
|
73
|
+
name: elementDef.name,
|
|
74
|
+
status: 'not_found',
|
|
75
|
+
selectors: {},
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
// Try CSS selectors
|
|
79
|
+
for (const check of elementDef.checks.filter(c => c.type === 'css')) {
|
|
80
|
+
try {
|
|
81
|
+
const element = await page.$(check.selector);
|
|
82
|
+
if (element) {
|
|
83
|
+
result.status = 'found';
|
|
84
|
+
result.selectors.css = check.selector;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Continue to next selector
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Try XPath selectors
|
|
93
|
+
for (const check of elementDef.checks.filter(c => c.type === 'xpath')) {
|
|
94
|
+
try {
|
|
95
|
+
const elements = await page.$x(check.selector);
|
|
96
|
+
if (elements.length > 0) {
|
|
97
|
+
if (result.status !== 'found') {
|
|
98
|
+
result.status = 'found';
|
|
99
|
+
}
|
|
100
|
+
result.selectors.xpath = check.selector;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Continue to next selector
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Try text-based detection
|
|
109
|
+
for (const check of elementDef.checks.filter(c => c.type === 'text')) {
|
|
110
|
+
try {
|
|
111
|
+
const hasText = await page.evaluate((text) => {
|
|
112
|
+
return document.body.innerText.includes(text);
|
|
113
|
+
}, check.contains);
|
|
114
|
+
if (hasText) {
|
|
115
|
+
if (result.status !== 'found') {
|
|
116
|
+
result.status = 'found';
|
|
117
|
+
}
|
|
118
|
+
result.selectors.accessibility = `Text contains: "${check.contains}"`;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Continue to next check
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// If we found some selectors but not all expected ones, mark as structure_changed
|
|
127
|
+
if (result.status === 'found' && Object.keys(result.selectors).length < elementDef.checks.length) {
|
|
128
|
+
result.status = 'structure_changed';
|
|
129
|
+
result.details = 'Some selectors work, but structure may have changed';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
result.details = `Error during detection: ${error instanceof Error ? error.message : String(error)}`;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Generate diagnostic report in Markdown format
|
|
139
|
+
*/
|
|
140
|
+
function generateReport(result) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
lines.push('# ChatGPT UI Diagnosis Report');
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(`**Date**: ${result.timestamp}`);
|
|
145
|
+
lines.push(`**URL**: ${result.url}`);
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push('## Element Detection Results');
|
|
148
|
+
lines.push('');
|
|
149
|
+
for (const element of result.elements) {
|
|
150
|
+
const statusEmoji = element.status === 'found' ? '✅' :
|
|
151
|
+
element.status === 'structure_changed' ? '⚠️' :
|
|
152
|
+
'❌';
|
|
153
|
+
lines.push(`### ${statusEmoji} ${element.name}`);
|
|
154
|
+
lines.push(`- **Status**: ${element.status.replace(/_/g, ' ')}`);
|
|
155
|
+
if (element.selectors && Object.keys(element.selectors).length > 0) {
|
|
156
|
+
lines.push('- **Current Selectors**:');
|
|
157
|
+
if (element.selectors.css) {
|
|
158
|
+
lines.push(` - CSS: \`${element.selectors.css}\``);
|
|
159
|
+
}
|
|
160
|
+
if (element.selectors.xpath) {
|
|
161
|
+
lines.push(` - XPath: \`${element.selectors.xpath}\``);
|
|
162
|
+
}
|
|
163
|
+
if (element.selectors.accessibility) {
|
|
164
|
+
lines.push(` - Accessibility: \`${element.selectors.accessibility}\``);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (element.details) {
|
|
168
|
+
lines.push(`- **Details**: ${element.details}`);
|
|
169
|
+
}
|
|
170
|
+
if (element.suggestion) {
|
|
171
|
+
lines.push(`- **Suggestion**: ${element.suggestion}`);
|
|
172
|
+
}
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
lines.push('## Files Generated');
|
|
176
|
+
lines.push('');
|
|
177
|
+
lines.push(`- **HTML**: ${result.filesGenerated.html}`);
|
|
178
|
+
lines.push(`- **Screenshot**: ${result.filesGenerated.screenshot}`);
|
|
179
|
+
lines.push(`- **AX Tree**: ${result.filesGenerated.axTree}`);
|
|
180
|
+
lines.push(`- **Report**: ${result.filesGenerated.report}`);
|
|
181
|
+
lines.push('');
|
|
182
|
+
lines.push('## Next Steps');
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push('1. Review the HTML snapshot to understand current DOM structure');
|
|
185
|
+
lines.push('2. Check the screenshot to see visual layout changes');
|
|
186
|
+
lines.push('3. Analyze the AX tree JSON for accessibility structure changes');
|
|
187
|
+
lines.push('4. Update selectors in relevant tool files based on findings');
|
|
188
|
+
lines.push('');
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
export const diagnoseChatgptUi = defineTool({
|
|
192
|
+
name: 'diagnose_chatgpt_ui',
|
|
193
|
+
description: 'Diagnose ChatGPT UI structure for debugging. ' +
|
|
194
|
+
'Captures full AX tree, HTML snapshot, screenshot, and generates a diagnostic report. ' +
|
|
195
|
+
'Use this when ChatGPT UI detection is failing or after ChatGPT updates.',
|
|
196
|
+
annotations: {
|
|
197
|
+
title: 'Diagnose ChatGPT UI',
|
|
198
|
+
category: ToolCategories.DEBUGGING,
|
|
199
|
+
readOnlyHint: true,
|
|
200
|
+
},
|
|
201
|
+
schema: {
|
|
202
|
+
url: z.string()
|
|
203
|
+
.default('https://chatgpt.com/')
|
|
204
|
+
.describe('ChatGPT URL to diagnose (default: https://chatgpt.com/)'),
|
|
205
|
+
waitForLoad: z.number()
|
|
206
|
+
.default(5000)
|
|
207
|
+
.describe('Time to wait for page to load in milliseconds (default: 5000)'),
|
|
208
|
+
},
|
|
209
|
+
handler: async (request, response, context) => {
|
|
210
|
+
const { url, waitForLoad } = request.params;
|
|
211
|
+
const page = context.getSelectedPage();
|
|
212
|
+
try {
|
|
213
|
+
response.appendResponseLine('🔍 Starting ChatGPT UI diagnosis...');
|
|
214
|
+
response.appendResponseLine('');
|
|
215
|
+
// Navigate to ChatGPT
|
|
216
|
+
response.appendResponseLine(`📡 Navigating to ${url}...`);
|
|
217
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
218
|
+
// Wait for page to stabilize
|
|
219
|
+
response.appendResponseLine(`⏳ Waiting ${waitForLoad}ms for page to stabilize...`);
|
|
220
|
+
await new Promise(resolve => setTimeout(resolve, waitForLoad));
|
|
221
|
+
// Ensure snapshot directory exists
|
|
222
|
+
const snapshotDir = await ensureSnapshotDir();
|
|
223
|
+
const timestamp = new Date().toISOString();
|
|
224
|
+
// Generate filenames
|
|
225
|
+
const htmlFilename = generateFilename('html');
|
|
226
|
+
const screenshotFilename = generateFilename('png');
|
|
227
|
+
const axFilename = generateFilename('ax.json');
|
|
228
|
+
const reportFilename = generateFilename('report.md');
|
|
229
|
+
// Capture HTML
|
|
230
|
+
response.appendResponseLine('📄 Capturing HTML snapshot...');
|
|
231
|
+
const htmlContent = await page.content();
|
|
232
|
+
const htmlPath = path.join(snapshotDir, htmlFilename);
|
|
233
|
+
await fs.promises.writeFile(htmlPath, htmlContent, 'utf-8');
|
|
234
|
+
response.appendResponseLine(`✅ HTML saved: ${htmlFilename}`);
|
|
235
|
+
// Take screenshot
|
|
236
|
+
response.appendResponseLine('📸 Taking screenshot...');
|
|
237
|
+
const screenshotBuffer = await page.screenshot({
|
|
238
|
+
fullPage: true,
|
|
239
|
+
type: 'png',
|
|
240
|
+
});
|
|
241
|
+
const screenshotPath = path.join(snapshotDir, screenshotFilename);
|
|
242
|
+
await fs.promises.writeFile(screenshotPath, screenshotBuffer);
|
|
243
|
+
response.appendResponseLine(`✅ Screenshot saved: ${screenshotFilename}`);
|
|
244
|
+
// Capture full AX tree
|
|
245
|
+
response.appendResponseLine('🌳 Capturing accessibility tree...');
|
|
246
|
+
const snapshot = await page.accessibility.snapshot({ interestingOnly: false });
|
|
247
|
+
const axPath = path.join(snapshotDir, axFilename);
|
|
248
|
+
await fs.promises.writeFile(axPath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
249
|
+
response.appendResponseLine(`✅ AX tree saved: ${axFilename}`);
|
|
250
|
+
// Detect known elements
|
|
251
|
+
response.appendResponseLine('');
|
|
252
|
+
response.appendResponseLine('🔎 Detecting important UI elements...');
|
|
253
|
+
const elementResults = [];
|
|
254
|
+
for (const elementDef of KNOWN_ELEMENTS) {
|
|
255
|
+
const result = await detectElement(page, elementDef);
|
|
256
|
+
elementResults.push(result);
|
|
257
|
+
const emoji = result.status === 'found' ? '✅' :
|
|
258
|
+
result.status === 'structure_changed' ? '⚠️' :
|
|
259
|
+
'❌';
|
|
260
|
+
response.appendResponseLine(`${emoji} ${result.name}: ${result.status}`);
|
|
261
|
+
}
|
|
262
|
+
// Generate diagnostic result
|
|
263
|
+
const diagnosticResult = {
|
|
264
|
+
timestamp,
|
|
265
|
+
url: page.url(),
|
|
266
|
+
elements: elementResults,
|
|
267
|
+
filesGenerated: {
|
|
268
|
+
html: htmlFilename,
|
|
269
|
+
screenshot: screenshotFilename,
|
|
270
|
+
axTree: axFilename,
|
|
271
|
+
report: reportFilename,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
// Generate and save report
|
|
275
|
+
response.appendResponseLine('');
|
|
276
|
+
response.appendResponseLine('📝 Generating diagnostic report...');
|
|
277
|
+
const reportContent = generateReport(diagnosticResult);
|
|
278
|
+
const reportPath = path.join(snapshotDir, reportFilename);
|
|
279
|
+
await fs.promises.writeFile(reportPath, reportContent, 'utf-8');
|
|
280
|
+
response.appendResponseLine(`✅ Report saved: ${reportFilename}`);
|
|
281
|
+
// Output summary
|
|
282
|
+
response.appendResponseLine('');
|
|
283
|
+
response.appendResponseLine('✅ Diagnosis complete!');
|
|
284
|
+
response.appendResponseLine('');
|
|
285
|
+
response.appendResponseLine('📁 All files saved to: docs/ui-snapshots/');
|
|
286
|
+
response.appendResponseLine('');
|
|
287
|
+
response.appendResponseLine('Summary:');
|
|
288
|
+
const foundCount = elementResults.filter(e => e.status === 'found').length;
|
|
289
|
+
const changedCount = elementResults.filter(e => e.status === 'structure_changed').length;
|
|
290
|
+
const notFoundCount = elementResults.filter(e => e.status === 'not_found').length;
|
|
291
|
+
response.appendResponseLine(`- ✅ Found: ${foundCount}`);
|
|
292
|
+
response.appendResponseLine(`- ⚠️ Changed: ${changedCount}`);
|
|
293
|
+
response.appendResponseLine(`- ❌ Not Found: ${notFoundCount}`);
|
|
294
|
+
// Attach screenshot to response
|
|
295
|
+
response.attachImage({
|
|
296
|
+
data: Buffer.from(screenshotBuffer).toString('base64'),
|
|
297
|
+
mimeType: 'image/png',
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
response.appendResponseLine('❌ Diagnosis failed:');
|
|
302
|
+
response.appendResponseLine(error instanceof Error ? error.message : String(error));
|
|
303
|
+
if (error instanceof Error && error.stack) {
|
|
304
|
+
response.appendResponseLine('');
|
|
305
|
+
response.appendResponseLine('Stack trace:');
|
|
306
|
+
response.appendResponseLine(error.stack);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
|
|
15
15
|
"start": "npm run build && node build/src/index.js",
|
|
16
16
|
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
|
|
17
|
+
"diagnose:ui": "echo 'Use diagnose_chatgpt_ui tool via MCP client (Claude Code)'",
|
|
17
18
|
"test": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test \"build/tests/**/*.test.js\"",
|
|
18
19
|
"test:only": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
|
|
19
20
|
"test:only:no-build": "node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
|