chrome-devtools-mcp-for-extension 0.9.22 → 0.9.24
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/index.js
CHANGED
|
File without changes
|
package/build/src/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import { McpResponse } from './McpResponse.js';
|
|
|
17
17
|
import { Mutex } from './Mutex.js';
|
|
18
18
|
import * as bookmarkTools from './tools/bookmarks.js';
|
|
19
19
|
import * as chatgptWebTools from './tools/chatgpt-web.js';
|
|
20
|
+
import * as deepResearchChatGPTTools from './tools/deep_research_chatgpt.js';
|
|
20
21
|
import * as consoleTools from './tools/console.js';
|
|
21
22
|
import * as emulationTools from './tools/emulation.js';
|
|
22
23
|
import * as extensionTools from './tools/extensions.js';
|
|
@@ -135,6 +136,7 @@ function registerTool(tool) {
|
|
|
135
136
|
const tools = [
|
|
136
137
|
...Object.values(bookmarkTools),
|
|
137
138
|
...Object.values(chatgptWebTools),
|
|
139
|
+
...Object.values(deepResearchChatGPTTools),
|
|
138
140
|
...Object.values(consoleTools),
|
|
139
141
|
...Object.values(emulationTools),
|
|
140
142
|
...Object.values(extensionTools),
|
|
@@ -0,0 +1,659 @@
|
|
|
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
|
+
* Path to store chat session data
|
|
13
|
+
*/
|
|
14
|
+
const CHAT_SESSIONS_FILE = path.join(process.cwd(), 'docs/ask/chatgpt/.chat-sessions.json');
|
|
15
|
+
/**
|
|
16
|
+
* Load chat sessions from JSON file
|
|
17
|
+
*/
|
|
18
|
+
async function loadChatSessions() {
|
|
19
|
+
try {
|
|
20
|
+
const data = await fs.promises.readFile(CHAT_SESSIONS_FILE, 'utf-8');
|
|
21
|
+
return JSON.parse(data);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Save a chat session for a project
|
|
29
|
+
*/
|
|
30
|
+
async function saveChatSession(projectName, session) {
|
|
31
|
+
const sessions = await loadChatSessions();
|
|
32
|
+
sessions[projectName] = session;
|
|
33
|
+
const dir = path.dirname(CHAT_SESSIONS_FILE);
|
|
34
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
35
|
+
await fs.promises.writeFile(CHAT_SESSIONS_FILE, JSON.stringify(sessions, null, 2), 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sanitize question to remove sensitive information
|
|
39
|
+
*/
|
|
40
|
+
function sanitizeQuestion(text) {
|
|
41
|
+
const passwordPatterns = [
|
|
42
|
+
/password\s*[:=]\s*\S+/gi,
|
|
43
|
+
/パスワード\s*[::=]\s*\S+/gi,
|
|
44
|
+
/pwd\s*[:=]\s*\S+/gi,
|
|
45
|
+
/secret\s*[:=]\s*\S+/gi,
|
|
46
|
+
];
|
|
47
|
+
let sanitized = text;
|
|
48
|
+
for (const pattern of passwordPatterns) {
|
|
49
|
+
sanitized = sanitized.replace(pattern, '[パスワードは除外されました]');
|
|
50
|
+
}
|
|
51
|
+
return sanitized;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save conversation log to docs/ask/chatgpt/
|
|
55
|
+
*/
|
|
56
|
+
async function saveConversationLog(projectName, question, response, metadata) {
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const timestamp = [
|
|
59
|
+
String(now.getFullYear()).slice(2).padStart(2, '0'),
|
|
60
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
61
|
+
String(now.getDate()).padStart(2, '0'),
|
|
62
|
+
'_',
|
|
63
|
+
String(now.getHours()).padStart(2, '0'),
|
|
64
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
65
|
+
String(now.getSeconds()).padStart(2, '0'),
|
|
66
|
+
].join('');
|
|
67
|
+
const topicSlug = question
|
|
68
|
+
.substring(0, 50)
|
|
69
|
+
.replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/gi, '-')
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.slice(0, 30);
|
|
72
|
+
const filename = `${timestamp}-${projectName}-deepresearch-${topicSlug}.md`;
|
|
73
|
+
const logDir = 'docs/ask/chatgpt';
|
|
74
|
+
const logPath = path.join(process.cwd(), logDir, filename);
|
|
75
|
+
await fs.promises.mkdir(path.dirname(logPath), { recursive: true });
|
|
76
|
+
const content = `# ${topicSlug}
|
|
77
|
+
|
|
78
|
+
## 📅 メタ情報
|
|
79
|
+
- **日時**: ${now.toLocaleString('ja-JP')}
|
|
80
|
+
- **プロジェクト**: ${projectName}
|
|
81
|
+
- **AIモデル**: ${metadata.model || 'ChatGPT DeepResearch'}
|
|
82
|
+
${metadata.researchTime ? `- **リサーチ時間**: ${metadata.researchTime}秒\n` : ''}${metadata.chatUrl ? `- **チャットURL**: ${metadata.chatUrl}\n` : ''}
|
|
83
|
+
## ❓ リサーチテーマ
|
|
84
|
+
|
|
85
|
+
${question}
|
|
86
|
+
|
|
87
|
+
## 🔍 DeepResearch 結果
|
|
88
|
+
|
|
89
|
+
${response}
|
|
90
|
+
`;
|
|
91
|
+
await fs.promises.writeFile(logPath, content, 'utf-8');
|
|
92
|
+
return path.relative(process.cwd(), logPath);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Detect if question is code-related
|
|
96
|
+
*/
|
|
97
|
+
function isCodeRelatedQuestion(question) {
|
|
98
|
+
const codeKeywords = [
|
|
99
|
+
'code',
|
|
100
|
+
'コード',
|
|
101
|
+
'programming',
|
|
102
|
+
'プログラミング',
|
|
103
|
+
'github',
|
|
104
|
+
'repository',
|
|
105
|
+
'リポジトリ',
|
|
106
|
+
'api',
|
|
107
|
+
'library',
|
|
108
|
+
'ライブラリ',
|
|
109
|
+
'framework',
|
|
110
|
+
'フレームワーク',
|
|
111
|
+
'typescript',
|
|
112
|
+
'javascript',
|
|
113
|
+
'python',
|
|
114
|
+
'implementation',
|
|
115
|
+
'実装',
|
|
116
|
+
'algorithm',
|
|
117
|
+
'アルゴリズム',
|
|
118
|
+
'database',
|
|
119
|
+
'データベース',
|
|
120
|
+
'function',
|
|
121
|
+
'関数',
|
|
122
|
+
];
|
|
123
|
+
const lowerQuestion = question.toLowerCase();
|
|
124
|
+
return codeKeywords.some((keyword) => lowerQuestion.includes(keyword));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Detect if currently in DeepResearch mode
|
|
128
|
+
*/
|
|
129
|
+
async function detectDeepResearchMode(page) {
|
|
130
|
+
return await page.evaluate(() => {
|
|
131
|
+
// Multi-language patterns for DeepResearch
|
|
132
|
+
const DEEP_RESEARCH_PATTERN = /deep\s*research|ディープ\s*リサーチ|深度研究|深入研究/i;
|
|
133
|
+
// Cache for performance (simple object-based cache instead of WeakRef for browser compatibility)
|
|
134
|
+
const cache = { timestamp: 0 };
|
|
135
|
+
const CACHE_TTL = 2000; // 2 seconds
|
|
136
|
+
// Step 1: Find scoped root element first
|
|
137
|
+
const findScopedRoot = () => {
|
|
138
|
+
// Try conversation root selectors
|
|
139
|
+
const candidates = [
|
|
140
|
+
document.querySelector('[data-testid="conversation-turns"]'),
|
|
141
|
+
document.querySelector('[role="main"]'),
|
|
142
|
+
document.querySelector('main'),
|
|
143
|
+
document.querySelector('[data-testid="conversation-root"]'),
|
|
144
|
+
];
|
|
145
|
+
return candidates.find((el) => el !== null) || document.body;
|
|
146
|
+
};
|
|
147
|
+
const scopedRoot = findScopedRoot();
|
|
148
|
+
if (!scopedRoot) {
|
|
149
|
+
return { isEnabled: false };
|
|
150
|
+
}
|
|
151
|
+
// Step 2: Try data-testid selectors FIRST (most reliable)
|
|
152
|
+
const dataTestIdSelectors = [
|
|
153
|
+
'[data-testid*="deep-research"]',
|
|
154
|
+
'[data-testid*="deepresearch"]',
|
|
155
|
+
'[data-testid*="research-mode"]',
|
|
156
|
+
];
|
|
157
|
+
for (const selector of dataTestIdSelectors) {
|
|
158
|
+
const element = scopedRoot.querySelector(selector);
|
|
159
|
+
if (element) {
|
|
160
|
+
return {
|
|
161
|
+
isEnabled: true,
|
|
162
|
+
indicator: `data-testid: ${element.getAttribute('data-testid')}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Step 3: Try aria-* attributes SECOND
|
|
167
|
+
const ariaSelectors = [
|
|
168
|
+
'[aria-label*="Deep Research" i]',
|
|
169
|
+
'[aria-label*="ディープリサーチ" i]',
|
|
170
|
+
'[aria-checked="true"][role="menuitemradio"]',
|
|
171
|
+
];
|
|
172
|
+
for (const selector of ariaSelectors) {
|
|
173
|
+
const elements = Array.from(scopedRoot.querySelectorAll(selector));
|
|
174
|
+
for (const element of elements) {
|
|
175
|
+
const ariaLabel = element.getAttribute('aria-label') || '';
|
|
176
|
+
const role = element.getAttribute('role') || '';
|
|
177
|
+
// Check aria-label with pattern
|
|
178
|
+
if (DEEP_RESEARCH_PATTERN.test(ariaLabel)) {
|
|
179
|
+
return {
|
|
180
|
+
isEnabled: true,
|
|
181
|
+
indicator: `aria-label: ${ariaLabel.substring(0, 50)}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// Check menuitemradio with aria-checked
|
|
185
|
+
if (role === 'menuitemradio') {
|
|
186
|
+
const isChecked = element.getAttribute('aria-checked') === 'true';
|
|
187
|
+
const text = element.textContent || '';
|
|
188
|
+
if (isChecked && DEEP_RESEARCH_PATTERN.test(text)) {
|
|
189
|
+
return {
|
|
190
|
+
isEnabled: true,
|
|
191
|
+
indicator: 'menuitemradio (checked)',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Step 4: Text matching as LAST resort (least reliable)
|
|
198
|
+
// Use cached result if available and fresh
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
if (cache.element && now - cache.timestamp < CACHE_TTL) {
|
|
201
|
+
return {
|
|
202
|
+
isEnabled: true,
|
|
203
|
+
indicator: 'cached indicator',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Search within scoped root only, not entire document
|
|
207
|
+
const textElements = Array.from(scopedRoot.querySelectorAll('div, span, button'));
|
|
208
|
+
for (const element of textElements) {
|
|
209
|
+
const text = element.textContent || '';
|
|
210
|
+
if (DEEP_RESEARCH_PATTERN.test(text)) {
|
|
211
|
+
// Update cache
|
|
212
|
+
cache.element = element;
|
|
213
|
+
cache.timestamp = now;
|
|
214
|
+
return {
|
|
215
|
+
isEnabled: true,
|
|
216
|
+
indicator: text.substring(0, 50),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { isEnabled: false };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Enable DeepResearch mode by clicking + button and selecting option
|
|
225
|
+
*/
|
|
226
|
+
async function enableDeepResearchMode(page, response) {
|
|
227
|
+
try {
|
|
228
|
+
response.appendResponseLine('DeepResearchモードを有効化中...');
|
|
229
|
+
// Step 1: Click "+" button
|
|
230
|
+
const plusClicked = await page.evaluate(() => {
|
|
231
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
232
|
+
const plusButton = buttons.find((btn) => {
|
|
233
|
+
const aria = btn.getAttribute('aria-label') || '';
|
|
234
|
+
const desc = btn.getAttribute('description') || '';
|
|
235
|
+
return aria.includes('ファイルの追加') || desc.includes('ファイルの追加');
|
|
236
|
+
});
|
|
237
|
+
if (!plusButton)
|
|
238
|
+
return { success: false, error: '+ボタンが見つかりません' };
|
|
239
|
+
plusButton.click();
|
|
240
|
+
return { success: true };
|
|
241
|
+
});
|
|
242
|
+
if (!plusClicked.success) {
|
|
243
|
+
return { success: false, error: plusClicked.error };
|
|
244
|
+
}
|
|
245
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
246
|
+
// Step 2: Select DeepResearch option
|
|
247
|
+
const deepResearchSelected = await page.evaluate(() => {
|
|
248
|
+
const menuItems = Array.from(document.querySelectorAll('[role="menuitemradio"]'));
|
|
249
|
+
const deepResearchItem = menuItems.find((item) => item.textContent?.includes('Deep Research') ||
|
|
250
|
+
item.textContent?.includes('ディープリサーチ'));
|
|
251
|
+
if (!deepResearchItem) {
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
error: 'DeepResearchオプションが見つかりません',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
deepResearchItem.click();
|
|
258
|
+
return { success: true };
|
|
259
|
+
});
|
|
260
|
+
if (!deepResearchSelected.success) {
|
|
261
|
+
return { success: false, error: deepResearchSelected.error };
|
|
262
|
+
}
|
|
263
|
+
response.appendResponseLine('✅ DeepResearchモード有効化');
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
265
|
+
return { success: true };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: error instanceof Error ? error.message : String(error),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Configure information sources (enable GitHub if needed)
|
|
276
|
+
*/
|
|
277
|
+
async function configureSources(page, response, enableGitHub) {
|
|
278
|
+
if (!enableGitHub) {
|
|
279
|
+
response.appendResponseLine('📚 情報源設定: Web (デフォルト)');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
response.appendResponseLine('📚 情報源設定: Web + GitHub (コード関連質問)');
|
|
283
|
+
const sourcesConfigured = await page.evaluate(() => {
|
|
284
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
285
|
+
const sourcesButton = buttons.find((btn) => btn.textContent?.includes('情報源'));
|
|
286
|
+
if (!sourcesButton) {
|
|
287
|
+
return { success: false, error: '情報源ボタンが見つかりません' };
|
|
288
|
+
}
|
|
289
|
+
sourcesButton.click();
|
|
290
|
+
return { success: true };
|
|
291
|
+
});
|
|
292
|
+
if (!sourcesConfigured.success) {
|
|
293
|
+
response.appendResponseLine(`⚠️ ${sourcesConfigured.error}`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
297
|
+
// Enable GitHub source
|
|
298
|
+
const githubEnabled = await page.evaluate(() => {
|
|
299
|
+
const checkboxes = Array.from(document.querySelectorAll('[role="menuitemcheckbox"]'));
|
|
300
|
+
const githubCheckbox = checkboxes.find((cb) => cb.textContent?.includes('GitHub'));
|
|
301
|
+
if (!githubCheckbox) {
|
|
302
|
+
return { success: false, error: 'GitHubオプションが見つかりません' };
|
|
303
|
+
}
|
|
304
|
+
const isChecked = githubCheckbox.getAttribute('aria-checked') === 'true';
|
|
305
|
+
if (!isChecked) {
|
|
306
|
+
githubCheckbox.click();
|
|
307
|
+
}
|
|
308
|
+
return { success: true, wasAlreadyEnabled: isChecked };
|
|
309
|
+
});
|
|
310
|
+
if (githubEnabled.success) {
|
|
311
|
+
response.appendResponseLine(githubEnabled.wasAlreadyEnabled
|
|
312
|
+
? '✅ GitHub情報源は既に有効です'
|
|
313
|
+
: '✅ GitHub情報源を有効化');
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
response.appendResponseLine(`⚠️ ${githubEnabled.error}`);
|
|
317
|
+
}
|
|
318
|
+
// Close menu
|
|
319
|
+
await page.keyboard.press('Escape');
|
|
320
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Send question text and click send button
|
|
324
|
+
*/
|
|
325
|
+
async function sendQuestion(page, response, question) {
|
|
326
|
+
response.appendResponseLine('リサーチテーマを送信中...');
|
|
327
|
+
const questionSent = await page.evaluate((questionText) => {
|
|
328
|
+
const prosemirror = document.querySelector('.ProseMirror[contenteditable="true"]');
|
|
329
|
+
if (!prosemirror)
|
|
330
|
+
return false;
|
|
331
|
+
prosemirror.innerHTML = '';
|
|
332
|
+
const p = document.createElement('p');
|
|
333
|
+
p.textContent = questionText;
|
|
334
|
+
prosemirror.appendChild(p);
|
|
335
|
+
prosemirror.dispatchEvent(new Event('input', { bubbles: true }));
|
|
336
|
+
return true;
|
|
337
|
+
}, question);
|
|
338
|
+
if (!questionSent) {
|
|
339
|
+
return { success: false, error: 'エディタが見つかりません' };
|
|
340
|
+
}
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
342
|
+
// Click send button
|
|
343
|
+
const sent = await page.evaluate(() => {
|
|
344
|
+
const sendButton = document.querySelector('button[data-testid="send-button"]');
|
|
345
|
+
if (sendButton && !sendButton.disabled) {
|
|
346
|
+
sendButton.click();
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
});
|
|
351
|
+
if (!sent) {
|
|
352
|
+
return { success: false, error: '送信ボタンが見つかりません' };
|
|
353
|
+
}
|
|
354
|
+
response.appendResponseLine('✅ リサーチテーマ送信完了');
|
|
355
|
+
return { success: true };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Handle conversation continuation until research starts
|
|
359
|
+
*/
|
|
360
|
+
async function handleConversationLoop(page, response, maxTurns = 5) {
|
|
361
|
+
response.appendResponseLine('💬 ChatGPTとの対話を開始(リサーチ開始まで継続)...');
|
|
362
|
+
let conversationTurns = 0;
|
|
363
|
+
while (conversationTurns < maxTurns) {
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
365
|
+
const status = await page.evaluate(() => {
|
|
366
|
+
// Check for research progress indicator
|
|
367
|
+
const progressIndicators = Array.from(document.querySelectorAll('div, span'));
|
|
368
|
+
const isResearching = progressIndicators.some((el) => el.textContent?.includes('リサーチ中') ||
|
|
369
|
+
el.textContent?.includes('Researching') ||
|
|
370
|
+
el.textContent?.includes('情報を収集中'));
|
|
371
|
+
if (isResearching) {
|
|
372
|
+
return { phase: 'researching' };
|
|
373
|
+
}
|
|
374
|
+
// Check if ChatGPT is asking a clarifying question
|
|
375
|
+
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
376
|
+
if (assistantMessages.length === 0) {
|
|
377
|
+
return { phase: 'waiting' };
|
|
378
|
+
}
|
|
379
|
+
const latestMessage = assistantMessages[assistantMessages.length - 1];
|
|
380
|
+
const messageText = latestMessage.textContent || '';
|
|
381
|
+
// Check if it's still streaming
|
|
382
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
383
|
+
const isStreaming = buttons.some((btn) => {
|
|
384
|
+
const text = btn.textContent || '';
|
|
385
|
+
const aria = btn.getAttribute('aria-label') || '';
|
|
386
|
+
return (text.includes('ストリーミングの停止') ||
|
|
387
|
+
text.includes('停止') ||
|
|
388
|
+
aria.includes('ストリーミングの停止') ||
|
|
389
|
+
aria.includes('停止'));
|
|
390
|
+
});
|
|
391
|
+
if (isStreaming) {
|
|
392
|
+
return { phase: 'streaming' };
|
|
393
|
+
}
|
|
394
|
+
// ChatGPT has asked a question
|
|
395
|
+
return {
|
|
396
|
+
phase: 'clarification',
|
|
397
|
+
question: messageText.substring(0, 200),
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
if (status.phase === 'researching') {
|
|
401
|
+
response.appendResponseLine('\n🔍 リサーチが開始されました!監視を開始...');
|
|
402
|
+
return { researchStarted: true };
|
|
403
|
+
}
|
|
404
|
+
if (status.phase === 'clarification') {
|
|
405
|
+
conversationTurns++;
|
|
406
|
+
response.appendResponseLine(`\n💬 ChatGPTの質問 (${conversationTurns}/${maxTurns}):`);
|
|
407
|
+
response.appendResponseLine(`"${status.question}..."`);
|
|
408
|
+
// Auto-respond to continue
|
|
409
|
+
response.appendResponseLine('自動応答: その内容で実施してください');
|
|
410
|
+
const responded = await page.evaluate(() => {
|
|
411
|
+
const prosemirror = document.querySelector('.ProseMirror[contenteditable="true"]');
|
|
412
|
+
if (!prosemirror)
|
|
413
|
+
return false;
|
|
414
|
+
prosemirror.innerHTML = '';
|
|
415
|
+
const p = document.createElement('p');
|
|
416
|
+
p.textContent = 'その内容で実施してください';
|
|
417
|
+
prosemirror.appendChild(p);
|
|
418
|
+
prosemirror.dispatchEvent(new Event('input', { bubbles: true }));
|
|
419
|
+
return true;
|
|
420
|
+
});
|
|
421
|
+
if (responded) {
|
|
422
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
423
|
+
await page.evaluate(() => {
|
|
424
|
+
const sendButton = document.querySelector('button[data-testid="send-button"]');
|
|
425
|
+
if (sendButton && !sendButton.disabled) {
|
|
426
|
+
sendButton.click();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
response.appendResponseLine('✅ 応答を送信');
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (status.phase === 'streaming' || status.phase === 'waiting') {
|
|
434
|
+
// Still processing, wait
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
researchStarted: false,
|
|
440
|
+
error: '会話ターン数が上限に達しました。リサーチが開始されませんでした。',
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Monitor research progress until completion
|
|
445
|
+
*/
|
|
446
|
+
async function monitorResearch(page, response, startTime) {
|
|
447
|
+
response.appendResponseLine('⏳ DeepResearchを実行中... (数分かかる場合があります)');
|
|
448
|
+
const MAX_WAIT_TIME = 15 * 60 * 1000; // 15 minutes max
|
|
449
|
+
let progressCounter = 0;
|
|
450
|
+
while (Date.now() - startTime < MAX_WAIT_TIME) {
|
|
451
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
452
|
+
const researchStatus = await page.evaluate(() => {
|
|
453
|
+
// Check if research completed
|
|
454
|
+
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
455
|
+
if (assistantMessages.length === 0) {
|
|
456
|
+
return { completed: false, stillResearching: true };
|
|
457
|
+
}
|
|
458
|
+
const latestMessage = assistantMessages[assistantMessages.length - 1];
|
|
459
|
+
// Check if still researching
|
|
460
|
+
const progressIndicators = Array.from(document.querySelectorAll('div, span'));
|
|
461
|
+
const isResearching = progressIndicators.some((el) => el.textContent?.includes('リサーチ中') ||
|
|
462
|
+
el.textContent?.includes('Researching') ||
|
|
463
|
+
el.textContent?.includes('情報を収集中'));
|
|
464
|
+
if (isResearching) {
|
|
465
|
+
return { completed: false, stillResearching: true };
|
|
466
|
+
}
|
|
467
|
+
// Check if streaming
|
|
468
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
469
|
+
const isStreaming = buttons.some((btn) => {
|
|
470
|
+
const text = btn.textContent || '';
|
|
471
|
+
const aria = btn.getAttribute('aria-label') || '';
|
|
472
|
+
return (text.includes('ストリーミングの停止') ||
|
|
473
|
+
aria.includes('ストリーミングの停止'));
|
|
474
|
+
});
|
|
475
|
+
if (isStreaming) {
|
|
476
|
+
return { completed: false, stillResearching: true };
|
|
477
|
+
}
|
|
478
|
+
// Research completed
|
|
479
|
+
return {
|
|
480
|
+
completed: true,
|
|
481
|
+
result: latestMessage.textContent || '',
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
if (researchStatus.completed) {
|
|
485
|
+
const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000);
|
|
486
|
+
response.appendResponseLine(`\n✅ DeepResearch完了 (所要時間: ${elapsedMinutes}分)`);
|
|
487
|
+
return {
|
|
488
|
+
completed: true,
|
|
489
|
+
result: researchStatus.result || '',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Show progress every 30 seconds
|
|
493
|
+
progressCounter++;
|
|
494
|
+
if (progressCounter % 6 === 0) {
|
|
495
|
+
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
496
|
+
response.appendResponseLine(`⏱️ ${elapsedSeconds}秒経過 - リサーチ継続中...`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
completed: false,
|
|
501
|
+
error: 'リサーチがタイムアウトしました(15分経過)',
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
export const deepResearchChatGPT = defineTool({
|
|
505
|
+
name: 'deep_research_chatgpt',
|
|
506
|
+
description: `Perform deep research using ChatGPT's DeepResearch mode. This tool automatically handles mode detection, source selection, conversation continuation, and result retrieval. Use this when thorough research is needed.`,
|
|
507
|
+
annotations: {
|
|
508
|
+
category: ToolCategories.NAVIGATION_AUTOMATION,
|
|
509
|
+
readOnlyHint: false,
|
|
510
|
+
},
|
|
511
|
+
schema: {
|
|
512
|
+
question: z
|
|
513
|
+
.string()
|
|
514
|
+
.describe('The research question or topic. Should be detailed and well-formed.'),
|
|
515
|
+
projectName: z
|
|
516
|
+
.string()
|
|
517
|
+
.optional()
|
|
518
|
+
.describe('Project name for organizing research sessions. Defaults to current working directory name.'),
|
|
519
|
+
enableGitHub: z
|
|
520
|
+
.boolean()
|
|
521
|
+
.optional()
|
|
522
|
+
.describe('Enable GitHub as information source. Auto-detected if question is code-related.'),
|
|
523
|
+
reuseSession: z
|
|
524
|
+
.boolean()
|
|
525
|
+
.optional()
|
|
526
|
+
.describe('Reuse existing project chat session instead of creating new chat. Default: false'),
|
|
527
|
+
},
|
|
528
|
+
handler: async (request, response, context) => {
|
|
529
|
+
const { question, projectName, enableGitHub, reuseSession = false } = request.params;
|
|
530
|
+
const sanitizedQuestion = sanitizeQuestion(question);
|
|
531
|
+
const project = projectName || path.basename(process.cwd()) || 'unknown-project';
|
|
532
|
+
// Auto-detect if GitHub should be enabled
|
|
533
|
+
const shouldEnableGitHub = enableGitHub !== undefined
|
|
534
|
+
? enableGitHub
|
|
535
|
+
: isCodeRelatedQuestion(question);
|
|
536
|
+
const page = context.getSelectedPage();
|
|
537
|
+
try {
|
|
538
|
+
// Phase 1: Navigate to ChatGPT
|
|
539
|
+
response.appendResponseLine('🔍 DeepResearchモードを開始...');
|
|
540
|
+
let needsNewChat = true;
|
|
541
|
+
if (reuseSession) {
|
|
542
|
+
// Try to load existing session
|
|
543
|
+
const sessions = await loadChatSessions();
|
|
544
|
+
const existingSession = sessions[project];
|
|
545
|
+
if (existingSession) {
|
|
546
|
+
response.appendResponseLine(`既存のプロジェクトチャットを使用: ${existingSession.url}`);
|
|
547
|
+
await page.goto(existingSession.url, { waitUntil: 'networkidle2' });
|
|
548
|
+
needsNewChat = false;
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
response.appendResponseLine('既存チャットが見つかりませんでした。新規作成します。');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (needsNewChat) {
|
|
556
|
+
await page.goto('https://chatgpt.com/', { waitUntil: 'networkidle2' });
|
|
557
|
+
}
|
|
558
|
+
// Check if logged in
|
|
559
|
+
const currentUrl = page.url();
|
|
560
|
+
if (currentUrl.includes('auth') || currentUrl.includes('login')) {
|
|
561
|
+
response.appendResponseLine('❌ ChatGPTにログインが必要です。ブラウザで手動ログインしてください。');
|
|
562
|
+
response.appendResponseLine(`ログインURL: ${currentUrl}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
response.appendResponseLine('✅ ログイン確認完了');
|
|
566
|
+
// Phase 2: Create new chat if needed
|
|
567
|
+
if (needsNewChat) {
|
|
568
|
+
response.appendResponseLine('新規チャットを作成中...');
|
|
569
|
+
await page.evaluate(() => {
|
|
570
|
+
const newChatLink = document.querySelector('a[href="/"]');
|
|
571
|
+
if (newChatLink) {
|
|
572
|
+
newChatLink.click();
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
576
|
+
// Turn off temporary chat
|
|
577
|
+
const tempChatDisabled = await page.evaluate(() => {
|
|
578
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
579
|
+
const btn = buttons.find((b) => {
|
|
580
|
+
const label = b.getAttribute('aria-label') || '';
|
|
581
|
+
return label.includes('一時チャットをオフにする');
|
|
582
|
+
});
|
|
583
|
+
if (btn) {
|
|
584
|
+
btn.click();
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
});
|
|
589
|
+
if (tempChatDisabled) {
|
|
590
|
+
response.appendResponseLine('✅ 一時チャット無効化');
|
|
591
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Phase 3: Detect and enable DeepResearch mode if needed
|
|
595
|
+
const modeStatus = await detectDeepResearchMode(page);
|
|
596
|
+
if (modeStatus.isEnabled) {
|
|
597
|
+
response.appendResponseLine(`✅ DeepResearchモード既に有効 (${modeStatus.indicator})`);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
response.appendResponseLine('DeepResearchモードが無効です。有効化します...');
|
|
601
|
+
const enableResult = await enableDeepResearchMode(page, response);
|
|
602
|
+
if (!enableResult.success) {
|
|
603
|
+
response.appendResponseLine(`❌ ${enableResult.error}`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Phase 4: Configure information sources
|
|
608
|
+
await configureSources(page, response, shouldEnableGitHub);
|
|
609
|
+
// Phase 5: Send research question
|
|
610
|
+
const sendResult = await sendQuestion(page, response, sanitizedQuestion);
|
|
611
|
+
if (!sendResult.success) {
|
|
612
|
+
response.appendResponseLine(`❌ ${sendResult.error}`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// Phase 6: Conversation continuation loop
|
|
616
|
+
const startTime = Date.now();
|
|
617
|
+
const loopResult = await handleConversationLoop(page, response);
|
|
618
|
+
if (!loopResult.researchStarted) {
|
|
619
|
+
response.appendResponseLine(`⚠️ ${loopResult.error}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// Phase 7: Monitor research progress
|
|
623
|
+
const monitorResult = await monitorResearch(page, response, startTime);
|
|
624
|
+
if (!monitorResult.completed) {
|
|
625
|
+
response.appendResponseLine(`❌ ${monitorResult.error}`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// Phase 8: Save results
|
|
629
|
+
const chatUrl = page.url();
|
|
630
|
+
const chatIdMatch = chatUrl.match(/\/c\/([a-f0-9-]+)/);
|
|
631
|
+
if (chatIdMatch) {
|
|
632
|
+
const chatId = chatIdMatch[1];
|
|
633
|
+
await saveChatSession(project, {
|
|
634
|
+
chatId,
|
|
635
|
+
url: chatUrl,
|
|
636
|
+
lastUsed: new Date().toISOString(),
|
|
637
|
+
title: `[DeepResearch: ${project}]`,
|
|
638
|
+
});
|
|
639
|
+
response.appendResponseLine(`💾 チャットセッション保存: ${chatId}`);
|
|
640
|
+
}
|
|
641
|
+
// Save conversation log
|
|
642
|
+
const logPath = await saveConversationLog(project, sanitizedQuestion, monitorResult.result || '', {
|
|
643
|
+
researchTime: Math.floor((Date.now() - startTime) / 1000),
|
|
644
|
+
chatUrl,
|
|
645
|
+
model: 'ChatGPT DeepResearch',
|
|
646
|
+
});
|
|
647
|
+
response.appendResponseLine(`📝 リサーチログ保存: ${logPath}`);
|
|
648
|
+
response.appendResponseLine(`🔗 チャットURL: ${chatUrl}`);
|
|
649
|
+
response.appendResponseLine('\n' + '='.repeat(60));
|
|
650
|
+
response.appendResponseLine('DeepResearch結果:\n');
|
|
651
|
+
response.appendResponseLine(monitorResult.result || '');
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
655
|
+
response.appendResponseLine(`❌ エラー: ${errorMessage}`);
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.24",
|
|
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",
|