feat(mcp): 新增多种 Gemini 工具,支持文本交互、图片上传提取及会话模型管理
This commit is contained in:
63
SKILL.md
63
SKILL.md
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: gemini-skill
|
name: gemini-skill
|
||||||
description: 通过 Gemini 官网(gemini.google.com)执行生图操作。用户提到"生图/画图/绘图/nano banana/nanobanana/生成图片"等关键词时触发。所有浏览器操作已封装为 MCP 工具,AI 无需手动操控浏览器,但必要时可以通过gemini_browser_info获取浏览器连接信息,如CDP连接端口,方便AI自行连接调试。
|
description: 通过 Gemini 官网(gemini.google.com)执行生图操作。用户提到"生图/画图/绘图/nano banana/nanobanana/生成图片"等关键词时触发。所有浏览器操作已封装为 MCP 工具,AI 无需手动操控浏览器,但必要时可以通过gemini_browser_info获取浏览器连接信息,方便AI自行连接调试。
|
||||||
---
|
---
|
||||||
|
|
||||||
# Gemini Skill
|
# Gemini Skill
|
||||||
@@ -18,22 +18,63 @@ description: 通过 Gemini 官网(gemini.google.com)执行生图操作。用
|
|||||||
|
|
||||||
### 可用工具
|
### 可用工具
|
||||||
|
|
||||||
|
**核心生图(封装完整流程):**
|
||||||
|
|
||||||
| 工具名 | 说明 | 入参 |
|
| 工具名 | 说明 | 入参 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| `gemini_generate_image` | 生成图片,返回本地文件路径 + base64 图片 | `prompt`(描述词),`newSession`(是否新建会话,默认 false) |
|
| `gemini_generate_image` | 完整生图流程:新建会话→发prompt→等待→提取图片→保存本地 | `prompt`,`newSession`(默认false),`referenceImages`(参考图路径数组,默认空) |
|
||||||
| `gemini_browser_info` | 获取浏览器连接信息(CDP 端口、wsEndpoint、Daemon 状态等) | 无 |
|
|
||||||
|
|
||||||
### 典型调用流程
|
**会话管理:**
|
||||||
|
|
||||||
1. 用户说"帮我画一张猫咪的图"
|
| 工具名 | 说明 | 入参 |
|
||||||
2. 调用 `gemini_generate_image`,传入 prompt
|
|--------|------|------|
|
||||||
3. 工具返回本地图片路径和 base64 数据
|
| `gemini_new_chat` | 新建一个空白对话 | 无 |
|
||||||
4. 将图片展示给用户
|
| `gemini_temp_chat` | 进入临时对话模式(不保留历史记录) | 无 |
|
||||||
|
|
||||||
### 参数说明
|
**模型切换:**
|
||||||
|
|
||||||
- `newSession: false`(默认)— 复用当前 Gemini 会话页,适合连续生图
|
| 工具名 | 说明 | 入参 |
|
||||||
- `newSession: true` — 新建干净会话,适合全新主题
|
|--------|------|------|
|
||||||
|
| `gemini_switch_model` | 切换 Gemini 模型 | `model`(`pro` / `quick` / `think`) |
|
||||||
|
|
||||||
|
**文本对话:**
|
||||||
|
|
||||||
|
| 工具名 | 说明 | 入参 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `gemini_send_message` | 发送文本消息并等待回答完成 | `message`,`timeout`(默认120000ms) |
|
||||||
|
|
||||||
|
**图片操作:**
|
||||||
|
|
||||||
|
| 工具名 | 说明 | 入参 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `gemini_upload_images` | 上传图片到输入框(仅上传不发送,可配合 send_message) | `images`(路径数组) |
|
||||||
|
| `gemini_get_images` | 获取会话中所有已加载图片的元信息 | 无 |
|
||||||
|
| `gemini_extract_image` | 提取指定图片的 base64 并保存到本地 | `imageUrl`(从 get_images 获取) |
|
||||||
|
|
||||||
|
**诊断 & 恢复:**
|
||||||
|
|
||||||
|
| 工具名 | 说明 | 入参 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `gemini_probe` | 探测页面各元素状态(输入框、按钮、模型等) | 无 |
|
||||||
|
| `gemini_reload_page` | 刷新页面(卡住或异常时使用) | `timeout`(默认30000ms) |
|
||||||
|
| `gemini_browser_info` | 获取浏览器连接信息(CDP 端口、wsEndpoint 等) | 无 |
|
||||||
|
|
||||||
|
### 典型用法
|
||||||
|
|
||||||
|
**快速生图(一步到位):**
|
||||||
|
1. 调用 `gemini_generate_image`,传入 prompt → 返回本地图片路径
|
||||||
|
|
||||||
|
**灵活组合(细粒度控制):**
|
||||||
|
1. `gemini_new_chat` — 新建会话
|
||||||
|
2. `gemini_switch_model` → `pro` — 切换到高质量模型
|
||||||
|
3. `gemini_upload_images` — 上传参考图
|
||||||
|
4. `gemini_send_message` — 发送描述词
|
||||||
|
5. `gemini_get_images` → `gemini_extract_image` — 获取并保存图片
|
||||||
|
|
||||||
|
**排障:**
|
||||||
|
1. `gemini_probe` — 看看页面元素有没有就位
|
||||||
|
2. `gemini_reload_page` — 页面卡了就刷新
|
||||||
|
3. `gemini_browser_info` — 获取 CDP 信息自行连接调试
|
||||||
|
|
||||||
## MCP 客户端配置
|
## MCP 客户端配置
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import puppeteerCore from 'puppeteer-core';
|
|||||||
import { addExtra } from 'puppeteer-extra';
|
import { addExtra } from 'puppeteer-extra';
|
||||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
import config from './config.js';
|
import config from './config.js';
|
||||||
|
import { sleep } from './util.js';
|
||||||
|
|
||||||
// connect 也套上 Stealth,双保险
|
// connect 也套上 Stealth,双保险
|
||||||
const puppeteer = addExtra(puppeteerCore);
|
const puppeteer = addExtra(puppeteerCore);
|
||||||
@@ -93,7 +94,7 @@ async function ensureDaemon() {
|
|||||||
// 轮询等待就绪
|
// 轮询等待就绪
|
||||||
const deadline = Date.now() + DAEMON_READY_TIMEOUT;
|
const deadline = Date.now() + DAEMON_READY_TIMEOUT;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await new Promise(r => setTimeout(r, DAEMON_POLL_INTERVAL));
|
await sleep(DAEMON_POLL_INTERVAL);
|
||||||
if (await isDaemonAlive()) {
|
if (await isDaemonAlive()) {
|
||||||
console.log('[browser] ✅ Daemon 就绪');
|
console.log('[browser] ✅ Daemon 就绪');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -409,6 +409,49 @@ export function createOps(page) {
|
|||||||
return isImageLoaded(op);
|
return isImageLoaded(op);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前会话中所有 Gemini 的文字回复
|
||||||
|
*
|
||||||
|
* 选择器:div.response-content
|
||||||
|
* 直接使用 innerText 提取渲染后的文本,浏览器排版引擎会自动处理换行和格式
|
||||||
|
*
|
||||||
|
* @returns {Promise<{ok: boolean, responses: Array<{index: number, text: string}>, total: number, error?: string}>}
|
||||||
|
*/
|
||||||
|
async getAllTextResponses() {
|
||||||
|
return op.query(() => {
|
||||||
|
const divs = [...document.querySelectorAll('div.response-content')];
|
||||||
|
if (!divs.length) {
|
||||||
|
return { ok: false, responses: [], total: 0, error: 'no_responses' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = divs.map((div, i) => ({
|
||||||
|
index: i,
|
||||||
|
text: (div.innerText || '').trim(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { ok: true, responses, total: responses.length };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最新一条 Gemini 文字回复
|
||||||
|
*
|
||||||
|
* 取最后一个 div.response-content,使用 innerText 提取渲染后的文本
|
||||||
|
*
|
||||||
|
* @returns {Promise<{ok: boolean, text?: string, index?: number, error?: string}>}
|
||||||
|
*/
|
||||||
|
async getLatestTextResponse() {
|
||||||
|
return op.query(() => {
|
||||||
|
const divs = [...document.querySelectorAll('div.response-content')];
|
||||||
|
if (!divs.length) {
|
||||||
|
return { ok: false, error: 'no_responses' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = divs[divs.length - 1];
|
||||||
|
return { ok: true, text: (last.innerText || '').trim(), index: divs.length - 1 };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取本次会话中所有已加载的图片
|
* 获取本次会话中所有已加载的图片
|
||||||
*
|
*
|
||||||
@@ -877,6 +920,4 @@ function isImageLoaded(op) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(r => setTimeout(r, ms));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,12 +23,39 @@ server.registerTool(
|
|||||||
newSession: z.boolean().default(false).describe(
|
newSession: z.boolean().default(false).describe(
|
||||||
"是否新建会话。true= 开启全新对话; false = 复用当前已有的 Gemini 会话页"
|
"是否新建会话。true= 开启全新对话; false = 复用当前已有的 Gemini 会话页"
|
||||||
),
|
),
|
||||||
|
referenceImages: z.array(z.string()).default([]).describe(
|
||||||
|
"参考图片的本地文件路径数组,例如 [\"/path/to/ref1.png\", \"/path/to/ref2.jpg\"]。图片会在发送 prompt 前上传到 Gemini 输入框"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ prompt, newSession }) => {
|
async ({ prompt, newSession, referenceImages }) => {
|
||||||
try {
|
try {
|
||||||
const { ops } = await createGeminiSession();
|
const { ops } = await createGeminiSession();
|
||||||
const result = await ops.generateImage(prompt, { newChat: newSession });
|
|
||||||
|
// 如果有参考图,先上传
|
||||||
|
if (referenceImages.length > 0) {
|
||||||
|
// 需要先处理新建会话(如果需要),因为 generateImage 内部的 newChat 会在上传之后才执行
|
||||||
|
if (newSession) {
|
||||||
|
await ops.click('newChatBtn');
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const imgPath of referenceImages) {
|
||||||
|
console.error(`[mcp] 正在上传参考图: ${imgPath}`);
|
||||||
|
const uploadResult = await ops.uploadImage(imgPath);
|
||||||
|
if (!uploadResult.ok) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `参考图上传失败: ${imgPath}\n错误: ${uploadResult.error}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error(`[mcp] ${referenceImages.length} 张参考图上传完成`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果上传了参考图且已手动新建会话,则 generateImage 内部不再新建
|
||||||
|
const needNewChat = referenceImages.length > 0 ? false : newSession;
|
||||||
|
const result = await ops.generateImage(prompt, { newChat: needNewChat });
|
||||||
|
|
||||||
// 执行完毕立刻断开,交还给 Daemon 倒计时
|
// 执行完毕立刻断开,交还给 Daemon 倒计时
|
||||||
disconnect();
|
disconnect();
|
||||||
@@ -71,6 +98,330 @@ server.registerTool(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ─── 会话管理 ───
|
||||||
|
|
||||||
|
// 新建会话
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_new_chat",
|
||||||
|
{
|
||||||
|
description: "在 Gemini 中新建一个空白对话",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.click('newChatBtn');
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `新建会话失败: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
return { content: [{ type: "text", text: "已新建 Gemini 会话" }] };
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 临时会话
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_temp_chat",
|
||||||
|
{
|
||||||
|
description: "进入 Gemini 临时对话模式(不保留历史记录,适合隐私场景)。注意:临时会话按钮仅在空白新会话页面可见,本工具会自动先新建会话再进入临时模式",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
|
||||||
|
// 临时会话按钮仅在空白新会话页可见,当前会话有内容时会被隐藏
|
||||||
|
// 因此必须先新建会话,确保页面回到空白状态
|
||||||
|
const newChatResult = await ops.click('newChatBtn');
|
||||||
|
if (!newChatResult.ok) {
|
||||||
|
disconnect();
|
||||||
|
return { content: [{ type: "text", text: `前置步骤失败:无法新建会话(临时会话按钮仅在空白页可见): ${newChatResult.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
// 等待新会话页面稳定
|
||||||
|
await sleep(250);
|
||||||
|
|
||||||
|
const result = await ops.clickTempChat();
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `进入临时会话失败: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
return { content: [{ type: "text", text: "已进入临时对话模式(自动先新建了空白会话)" }] };
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 模型切换 ───
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_switch_model",
|
||||||
|
{
|
||||||
|
description: "切换 Gemini 模型(pro / quick / think)",
|
||||||
|
inputSchema: {
|
||||||
|
model: z.enum(["pro", "quick", "think"]).describe("目标模型:pro=高质量, quick=快速, think=深度思考"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ model }) => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.switchToModel(model);
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `切换模型失败: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `模型已切换到 ${model}${result.previousModel ? `(之前是 ${result.previousModel})` : ''}` }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 文本对话 ───
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_send_message",
|
||||||
|
{
|
||||||
|
description: "向 Gemini 发送文本消息并等待回答完成(不提取图片,纯文本交互)",
|
||||||
|
inputSchema: {
|
||||||
|
message: z.string().describe("要发送给 Gemini 的文本内容"),
|
||||||
|
timeout: z.number().default(120000).describe("等待回答完成的超时时间(毫秒),默认 120000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ message, timeout }) => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.sendAndWait(message, { timeout });
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `发送失败: ${result.error},耗时 ${result.elapsed}ms` }], isError: true };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `消息已发送并等待完成,耗时 ${result.elapsed}ms` }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 图片上传 ───
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_upload_images",
|
||||||
|
{
|
||||||
|
description: "向 Gemini 当前输入框上传图片(仅上传,不发送消息),可配合 gemini_send_message 组合使用",
|
||||||
|
inputSchema: {
|
||||||
|
images: z.array(z.string()).min(1).describe("本地图片文件路径数组"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ images }) => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const imgPath of images) {
|
||||||
|
console.error(`[mcp] 正在上传: ${imgPath}`);
|
||||||
|
const r = await ops.uploadImage(imgPath);
|
||||||
|
results.push({ path: imgPath, ...r });
|
||||||
|
if (!r.ok) {
|
||||||
|
disconnect();
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `上传失败: ${imgPath}\n错误: ${r.error}\n\n已成功上传 ${results.filter(x => x.ok).length}/${images.length} 张` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `全部 ${images.length} 张图片上传成功` }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 图片获取 ───
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_get_images",
|
||||||
|
{
|
||||||
|
description: "获取当前 Gemini 会话中所有已加载的图片列表(不下载,仅返回元信息)",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.getAllImages();
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `未找到图片: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify({ total: result.total, newCount: result.newCount, images: result.images }, null, 2) }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_extract_image",
|
||||||
|
{
|
||||||
|
description: "提取指定图片的 base64 数据并保存到本地文件。可从 gemini_get_images 获取图片 src URL",
|
||||||
|
inputSchema: {
|
||||||
|
imageUrl: z.string().describe("图片的 src URL(从 gemini_get_images 结果中获取)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ imageUrl }) => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.extractImageBase64(imageUrl);
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `图片提取失败: ${result.error}${result.detail ? ' — ' + result.detail : ''}` }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地
|
||||||
|
const base64Data = result.dataUrl.split(',')[1];
|
||||||
|
const mimeMatch = result.dataUrl.match(/^data:(image\/\w+);/);
|
||||||
|
const ext = mimeMatch ? mimeMatch[1].split('/')[1] : 'png';
|
||||||
|
|
||||||
|
mkdirSync(config.outputDir, { recursive: true });
|
||||||
|
const filename = `gemini_${Date.now()}.${ext}`;
|
||||||
|
const filePath = join(config.outputDir, filename);
|
||||||
|
writeFileSync(filePath, Buffer.from(base64Data, 'base64'));
|
||||||
|
|
||||||
|
console.error(`[mcp] 图片已保存至 ${filePath}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: `图片提取成功,已保存至: ${filePath}` },
|
||||||
|
{ type: "image", data: base64Data, mimeType: mimeMatch ? mimeMatch[1] : "image/png" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 文字回复获取 ───
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_get_all_text_responses",
|
||||||
|
{
|
||||||
|
description: "获取当前 Gemini 会话中所有文字回复内容(仅文字,不含图片等其他类型回复)",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.getAllTextResponses();
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `未找到回复: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_get_latest_text_response",
|
||||||
|
{
|
||||||
|
description: "获取当前 Gemini 会话中最新一条文字回复(仅文字,不含图片等其他类型回复)",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.getLatestTextResponse();
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `未找到回复: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: result.text }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 页面状态 & 恢复 ───
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_probe",
|
||||||
|
{
|
||||||
|
description: "探测 Gemini 页面各元素状态(输入框、按钮、当前模型等),用于调试和排查问题",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.probe();
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gemini_reload_page",
|
||||||
|
{
|
||||||
|
description: "刷新 Gemini 页面(页面卡住或状态异常时使用)",
|
||||||
|
inputSchema: {
|
||||||
|
timeout: z.number().default(30000).describe("等待页面重新加载完成的超时(毫秒),默认 30000"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ timeout }) => {
|
||||||
|
try {
|
||||||
|
const { ops } = await createGeminiSession();
|
||||||
|
const result = await ops.reloadPage({ timeout });
|
||||||
|
disconnect();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { content: [{ type: "text", text: `页面刷新失败: ${result.error}` }], isError: true };
|
||||||
|
}
|
||||||
|
return { content: [{ type: "text", text: `页面刷新完成,耗时 ${result.elapsed}ms` }] };
|
||||||
|
} catch (err) {
|
||||||
|
return { content: [{ type: "text", text: `执行崩溃: ${err.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── 浏览器信息 ───
|
||||||
|
|
||||||
// 查询浏览器信息
|
// 查询浏览器信息
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"gemini_browser_info",
|
"gemini_browser_info",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
* - 鼠标 / 键盘事件通过 CDP Input 域发送,生成 isTrusted=true 的原生事件
|
* - 鼠标 / 键盘事件通过 CDP Input 域发送,生成 isTrusted=true 的原生事件
|
||||||
* - 每个方法都是独立的原子操作,上层 gemini-ops.js 负责编排组合
|
* - 每个方法都是独立的原子操作,上层 gemini-ops.js 负责编排组合
|
||||||
*/
|
*/
|
||||||
|
import { sleep } from './util.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 operator 实例
|
* 创建 operator 实例
|
||||||
@@ -92,7 +93,7 @@ export function createOperator(page) {
|
|||||||
*/
|
*/
|
||||||
function randomDelay(min, max) {
|
function randomDelay(min, max) {
|
||||||
const ms = min + Math.random() * (max - min);
|
const ms = min + Math.random() * (max - min);
|
||||||
return new Promise(r => setTimeout(r, ms));
|
return sleep(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 公开 API ───
|
// ─── 公开 API ───
|
||||||
@@ -264,7 +265,7 @@ export function createOperator(page) {
|
|||||||
return { ok: true, result, elapsed: Date.now() - start };
|
return { ok: true, result, elapsed: Date.now() - start };
|
||||||
}
|
}
|
||||||
} catch { /* 页面可能还在加载 */ }
|
} catch { /* 页面可能还在加载 */ }
|
||||||
await new Promise(r => setTimeout(r, interval));
|
await sleep(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: false, error: 'timeout', elapsed: Date.now() - start };
|
return { ok: false, error: 'timeout', elapsed: Date.now() - start };
|
||||||
|
|||||||
12
src/util.js
Normal file
12
src/util.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* util.js — 公共工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步等待指定毫秒数
|
||||||
|
* @param {number} ms
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export function sleep(ms) {
|
||||||
|
return new Promise(r => setTimeout(r, ms));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user