213 lines
7.2 KiB
JavaScript
213 lines
7.2 KiB
JavaScript
/**
|
||
* demo.js — 使用示例
|
||
*
|
||
* 两种启动方式:
|
||
*
|
||
* 方式 1(推荐):先手动启动浏览器,再运行 demo
|
||
* chrome --remote-debugging-port=9223 --user-data-dir="~/.gemini-skill/browser-data"
|
||
* (也可以用 Edge:msedge --remote-debugging-port=9223 --user-data-dir=...)
|
||
* node src/demo.js
|
||
*
|
||
* 方式 2:让 skill 自动检测并启动浏览器
|
||
* node src/demo.js
|
||
* (或指定路径:BROWSER_PATH="C:/..." node src/demo.js)
|
||
*
|
||
* 所有配置项见 .env,可直接编辑或通过命令行设环境变量。
|
||
*/
|
||
import { execSync } from 'node:child_process';
|
||
import { platform, homedir } from 'node:os';
|
||
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { createGeminiSession, disconnect } from './index.js';
|
||
|
||
const prompt = 'Gemini你好!请你仿造这个风格,给我生成更多表情包吧!来一张玩手机中。。。';
|
||
|
||
// ── Demo 专用:杀掉所有 Chromium 系浏览器进程 ──
|
||
function killAllBrowserProcesses() {
|
||
const os = platform();
|
||
const commands = os === 'win32'
|
||
? [
|
||
'taskkill /F /IM msedge.exe /T',
|
||
'taskkill /F /IM chrome.exe /T',
|
||
'taskkill /F /IM chromium.exe /T',
|
||
]
|
||
: [
|
||
'pkill -f msedge || true',
|
||
'pkill -f chrome || true',
|
||
'pkill -f chromium || true',
|
||
];
|
||
|
||
for (const cmd of commands) {
|
||
try {
|
||
execSync(cmd, { stdio: 'ignore', timeout: 5000 });
|
||
} catch {
|
||
// 进程不存在时会报错,忽略
|
||
}
|
||
}
|
||
console.log('[demo] 已清理所有浏览器进程');
|
||
}
|
||
|
||
/** 异步等待 */
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
/**
|
||
* 创建会话,如果因浏览器目录被锁而失败,自动杀掉全部浏览器进程后重试一次
|
||
*/
|
||
async function createSessionWithRetry() {
|
||
// 禁止 Puppeteer 在 Ctrl+C 等信号时自动杀浏览器进程,
|
||
// 由 demo 自己处理 SIGINT → disconnect,浏览器保持运行可复用。
|
||
const opts = {
|
||
debugOpts: {
|
||
handleSIGINT: false,
|
||
handleSIGTERM: false,
|
||
handleSIGHUP: false,
|
||
},
|
||
};
|
||
|
||
try {
|
||
return await createGeminiSession(opts);
|
||
} catch (err) {
|
||
const msg = err.message || '';
|
||
const isLocked = msg.includes('EPERM') || msg.includes('lock') || msg.includes('already');
|
||
|
||
if (!isLocked) throw err;
|
||
|
||
console.warn(
|
||
`[demo] 浏览器数据目录被占用,正在清理所有浏览器进程后重试...\n` +
|
||
` 原始错误:${msg}`
|
||
);
|
||
|
||
killAllBrowserProcesses();
|
||
await sleep(2000);
|
||
|
||
// 重试一次,还失败就直接抛出
|
||
return await createGeminiSession(opts);
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
console.log('=== Gemini Skill Demo ===\n');
|
||
|
||
// 创建会话:自带杀进程重试逻辑
|
||
const { ops } = await createSessionWithRetry();
|
||
|
||
// ── Ctrl+C 时只断开连接,不杀浏览器进程(下次可复用) ──
|
||
process.on('SIGINT', () => {
|
||
console.log('\n[demo] Ctrl+C 收到,断开浏览器连接(浏览器保持运行)...');
|
||
disconnect();
|
||
process.exit(0);
|
||
});
|
||
|
||
try {
|
||
// 1. 进入临时会话(不留聊天记录,保持账号干净)
|
||
console.log('[1] 进入临时会话...');
|
||
const tempResult = await ops.clickTempChat();
|
||
if (!tempResult.ok) {
|
||
console.warn('[1] 临时会话按钮未找到,跳过(可能已在临时模式或页面结构变化)');
|
||
} else {
|
||
console.log('[1] 已进入临时会话');
|
||
}
|
||
|
||
// 2. 探测页面状态
|
||
console.log('\n[2] 探测页面元素...');
|
||
const probe = await ops.probe();
|
||
console.log('probe:', JSON.stringify(probe, null, 2));
|
||
|
||
// 3. 确保使用 Pro 模型
|
||
console.log('\n[3] 检查模型...');
|
||
if (probe.currentModel.toLowerCase() === 'pro') {
|
||
console.log('[3] 当前已是 Pro 模型,跳过');
|
||
} else {
|
||
console.log(`[3] 当前模型: ${probe.currentModel || '未知'},切换到 Pro...`);
|
||
const switchResult = await ops.ensureModelPro();
|
||
if (switchResult.ok) {
|
||
console.log(`[3] 已切换到 Pro(之前: ${switchResult.previousModel || '未知'})`);
|
||
} else {
|
||
console.warn(`[3] 切换 Pro 失败: ${switchResult.error},继续使用当前模型`);
|
||
}
|
||
}
|
||
|
||
// 4. 上传图片
|
||
console.log('\n[4] 上传图片...');
|
||
|
||
const uploadResult = await ops.uploadImage('./gemini-image/miku_fighting.jpg');
|
||
if (uploadResult.ok) {
|
||
console.log(`[4] ✅ 图片上传完成 (${uploadResult.elapsed}ms)`);
|
||
if (uploadResult.warning) console.warn(`[4] ⚠ ${uploadResult.warning}`);
|
||
} else {
|
||
console.warn(`[4] ⚠ 图片上传失败: ${uploadResult.error} — ${uploadResult.detail}`);
|
||
}
|
||
|
||
// 5. 发送一句话
|
||
console.log('\n[5] 发送提示词...');
|
||
const result = await ops.sendAndWait(prompt, {
|
||
timeout: 120_000,
|
||
onPoll(poll) {
|
||
console.log(` polling... status=${poll.status}`);
|
||
},
|
||
});
|
||
console.log('result:', JSON.stringify(result, null, 2));
|
||
|
||
// 6. 等待图片加载完成
|
||
if (result.ok) {
|
||
console.log('\n[6] 等待图片加载完成...');
|
||
const imgLoadStart = Date.now();
|
||
while (Date.now() - imgLoadStart < 30_000) {
|
||
const { loaded } = await ops.checkImageLoaded();
|
||
if (loaded) break;
|
||
console.log(' 图片加载中...');
|
||
await sleep(500);
|
||
}
|
||
console.log(`[6] 图片加载完成 (${Date.now() - imgLoadStart}ms)`);
|
||
|
||
// 7. 获取最新图片并保存到本地
|
||
console.log('\n[7] 查找最新生成的图片...');
|
||
|
||
const imgInfo = await ops.getLatestImage();
|
||
console.log('imgInfo:', JSON.stringify(imgInfo, null, 2));
|
||
|
||
if (imgInfo.ok && imgInfo.src) {
|
||
console.log(`[7] 找到图片 (${imgInfo.width}x${imgInfo.height}, isNew=${imgInfo.isNew})`);
|
||
|
||
// 提取 base64 数据
|
||
console.log(`[7] 提取图片数据 (src=${imgInfo.src})...`);
|
||
const b64Result = await ops.extractImageBase64(imgInfo.src);
|
||
|
||
if (b64Result.ok && b64Result.dataUrl) {
|
||
// dataUrl 格式: data:image/png;base64,iVBOR...
|
||
const matches = b64Result.dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
|
||
if (matches) {
|
||
const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1];
|
||
const base64Data = matches[2];
|
||
const buffer = Buffer.from(base64Data, 'base64');
|
||
|
||
// 保存到 ./gemini-image/
|
||
const outputDir = './gemini-image';
|
||
if (!existsSync(outputDir)) {
|
||
mkdirSync(outputDir, { recursive: true });
|
||
}
|
||
const filename = `gemini_${Date.now()}.${ext}`;
|
||
const filepath = join(outputDir, filename);
|
||
|
||
writeFileSync(filepath, buffer);
|
||
console.log(`[7] ✅ 图片已保存: ${filepath} (${(buffer.length / 1024).toFixed(1)} KB, method=${b64Result.method})`);
|
||
} else {
|
||
console.warn('[7] ⚠ dataUrl 格式无法解析');
|
||
}
|
||
} else {
|
||
console.warn(`[7] ⚠ 提取图片数据失败: ${b64Result.error || 'unknown'}`);
|
||
}
|
||
} else {
|
||
console.log('[7] 未找到图片(可能本次回答不含图片)');
|
||
}
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Error:', err);
|
||
}
|
||
|
||
console.log('\n[done] 功能执行完毕,浏览器保持运行。按 Ctrl+C 退出。');
|
||
}
|
||
|
||
main().catch(console.error);
|