feat(gemini-ops): 新增页面导航功能,仅允许 gemini.google.com 域名

This commit is contained in:
WJZ_P
2026-03-24 11:40:07 +08:00
parent d4ad3262df
commit ad71636d5b
3 changed files with 74 additions and 1 deletions

View File

@@ -98,6 +98,12 @@ MCP 工具调用尤其是生图、等待回复等可能耗时较长60~1
|--------|------|------| |--------|------|------|
| `gemini_check_login` | 检查是否已登录 Google 账号 | 无 | | `gemini_check_login` | 检查是否已登录 Google 账号 | 无 |
**页面导航:**
| 工具名 | 说明 | 入参 |
|--------|------|------|
| `gemini_navigate_to` | 打开指定的 Gemini 页面 URL如历史会话链接仅允许 gemini.google.com 域名 | `url`(目标 URL`timeout`默认30000ms |
**诊断 & 恢复:** **诊断 & 恢复:**
| 工具名 | 说明 | 入参 | | 工具名 | 说明 | 入参 |

View File

@@ -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 输入框 * 上传图片到 Gemini 输入框
* *

View File

@@ -45,7 +45,7 @@ server.registerTool(
inputSchema: { inputSchema: {
prompt: z.string().describe("图片的详细描述词。提示:描述越详细越好,包含风格、构图、色调等关键词能显著提升生成质量"), prompt: z.string().describe("图片的详细描述词。提示:描述越详细越好,包含风格、构图、色调等关键词能显著提升生成质量"),
newSession: z.boolean().default(false).describe( newSession: z.boolean().default(false).describe(
"是否新建会话。true= 开启全新对话(推荐生成全新图片时使用); false= 复用当前会话(适合基于上下文迭代修改)" "是否新建会话。true= 开启全新对话(推荐生成全新图片时使用); false= 复用当前会话(适合基于上下文迭代修改,默认应该为"
), ),
referenceImages: z.array(z.string()).default([]).describe( referenceImages: z.array(z.string()).default([]).describe(
"参考图片的本地文件路径数组,例如 [\"/path/to/ref1.png\", \"/path/to/ref2.jpg\"]。图片会在发送 prompt 前上传到 Gemini 输入框" "参考图片的本地文件路径数组,例如 [\"/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 };
}
}
);
// ─── 浏览器信息 ─── // ─── 浏览器信息 ───
// 查询浏览器信息 // 查询浏览器信息