chrome-devtools-mcp-for-extension 0.11.1 → 0.12.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/main.js +12 -0
- 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/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,6 +58,7 @@ 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
63
|
const browser = await resolveBrowser({
|
|
61
64
|
browserUrl: args.browserUrl,
|
|
@@ -74,6 +77,14 @@ async function getContext() {
|
|
|
74
77
|
// Always recreate context if browser reference changed or context doesn't exist
|
|
75
78
|
if (!context || context.browser !== browser) {
|
|
76
79
|
context = await McpContext.from(browser, logger);
|
|
80
|
+
// Run UI health check only once per browser instance
|
|
81
|
+
if (!uiHealthCheckRun) {
|
|
82
|
+
uiHealthCheckRun = true;
|
|
83
|
+
// Run in background to avoid blocking startup
|
|
84
|
+
runStartupCheck(browser).catch(err => {
|
|
85
|
+
logger(`Startup check failed: ${err}`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
77
88
|
}
|
|
78
89
|
return context;
|
|
79
90
|
}
|
|
@@ -138,6 +149,7 @@ const tools = [
|
|
|
138
149
|
...Object.values(chatgptWebTools),
|
|
139
150
|
...Object.values(deepResearchChatGPTTools),
|
|
140
151
|
...Object.values(consoleTools),
|
|
152
|
+
...Object.values(diagnoseUiTools),
|
|
141
153
|
...Object.values(emulationTools),
|
|
142
154
|
...Object.values(extensionTools),
|
|
143
155
|
...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.12.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\"",
|