diff --git a/SKILL.md b/SKILL.md index f97c9df..970c4e5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -98,6 +98,12 @@ MCP 工具调用(尤其是生图、等待回复等)可能耗时较长(60~1 |--------|------|------| | `gemini_check_login` | 检查是否已登录 Google 账号 | 无 | +**页面导航:** + +| 工具名 | 说明 | 入参 | +|--------|------|------| +| `gemini_navigate_to` | 打开指定的 Gemini 页面 URL(如历史会话链接),仅允许 gemini.google.com 域名 | `url`(目标 URL),`timeout`(默认30000ms) | + **诊断 & 恢复:** | 工具名 | 说明 | 入参 | diff --git a/src/gemini-ops.js b/src/gemini-ops.js index bc11c14..6c41024 100644 --- a/src/gemini-ops.js +++ b/src/gemini-ops.js @@ -905,6 +905,40 @@ export function createOps(page) { } }, + /** + * 导航到指定的 Gemini 页面 URL + * + * 仅允许 gemini.google.com 域名下的地址(如指定会话 URL), + * 其他域名会直接拒绝,防止浏览器被劫持到不安全页面。 + * + * @param {string} url - 目标 URL,必须是 gemini.google.com 域名 + * @param {object} [options] + * @param {number} [options.timeout=30000] - 等待页面加载的超时时间(ms) + * @returns {Promise<{ok: boolean, url?: string, elapsed?: number, error?: string, detail?: string}>} + */ + async navigateTo(url, { timeout = 30_000 } = {}) { + try { + // 域名白名单校验 + const parsed = new URL(url); + if (parsed.hostname !== 'gemini.google.com') { + return { + ok: false, + error: 'invalid_domain', + detail: `仅允许 gemini.google.com 域名,收到: ${parsed.hostname}`, + }; + } + + const start = Date.now(); + await page.goto(url, { waitUntil: 'networkidle2', timeout }); + const elapsed = Date.now() - start; + const finalUrl = page.url(); + console.log(`[ops] 页面导航完成 → ${finalUrl} (${elapsed}ms)`); + return { ok: true, url: finalUrl, elapsed }; + } catch (e) { + return { ok: false, error: 'navigate_failed', detail: e.message }; + } + }, + /** * 上传图片到 Gemini 输入框 * diff --git a/src/mcp-server.js b/src/mcp-server.js index f636c6e..143427c 100644 --- a/src/mcp-server.js +++ b/src/mcp-server.js @@ -45,7 +45,7 @@ server.registerTool( inputSchema: { prompt: z.string().describe("图片的详细描述词。提示:描述越详细越好,包含风格、构图、色调等关键词能显著提升生成质量"), newSession: z.boolean().default(false).describe( - "是否新建会话。true= 开启全新对话(推荐生成全新图片时使用); false= 复用当前会话(适合基于上下文迭代修改)" + "是否新建会话。true= 开启全新对话(推荐生成全新图片时使用); false= 复用当前会话(适合基于上下文迭代修改,默认应该为)" ), referenceImages: z.array(z.string()).default([]).describe( "参考图片的本地文件路径数组,例如 [\"/path/to/ref1.png\", \"/path/to/ref2.jpg\"]。图片会在发送 prompt 前上传到 Gemini 输入框" @@ -540,6 +540,39 @@ server.registerTool( } ); +// ─── 页面导航 ─── + +server.registerTool( + "gemini_navigate_to", + { + description: "打开指定的 Gemini 页面 URL(如特定会话链接)。仅允许 gemini.google.com 域名,其他域名会被拒绝。适用于需要恢复到某个历史会话继续对话的场景", + inputSchema: { + url: z.string().url().describe( + "目标 Gemini URL,例如 https://gemini.google.com/app/57ace74d20f70d13 。必须是 gemini.google.com 域名" + ), + timeout: z.number().default(30000).describe("等待页面加载完成的超时(毫秒),默认 30000"), + }, + }, + async ({ url, timeout }) => { + try { + const { ops } = await createGeminiSession(); + const result = await ops.navigateTo(url, { timeout }); + disconnect(); + + if (!result.ok) { + let msg = `页面导航失败: ${result.error}`; + if (result.detail) msg += `\n${result.detail}`; + return { content: [{ type: "text", text: msg }], isError: true }; + } + return { + content: [{ type: "text", text: `已导航至: ${result.url}(耗时 ${result.elapsed}ms)` }], + }; + } catch (err) { + return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true }; + } + } +); + // ─── 浏览器信息 ─── // 查询浏览器信息