feat(ops): add image upload function
This commit is contained in:
34
src/demo.js
34
src/demo.js
@@ -127,8 +127,19 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发送一句话
|
// 4. 上传图片
|
||||||
console.log('\n[4] 发送提示词...');
|
console.log('\n[4] 上传图片...');
|
||||||
|
|
||||||
|
const uploadResult = await ops.uploadImage('./gemini-image/tianyi.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, {
|
const result = await ops.sendAndWait(prompt, {
|
||||||
timeout: 120_000,
|
timeout: 120_000,
|
||||||
onPoll(poll) {
|
onPoll(poll) {
|
||||||
@@ -137,19 +148,18 @@ async function main() {
|
|||||||
});
|
});
|
||||||
console.log('result:', JSON.stringify(result, null, 2));
|
console.log('result:', JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
// 5. 获取最新图片并保存到本地
|
// 6. 获取最新图片并保存到本地
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
console.log('\n[5] 查找最新生成的图片...');
|
console.log('\n[6] 查找最新生成的图片...');
|
||||||
await sleep(2000); // 等待图片渲染完毕
|
|
||||||
|
|
||||||
const imgInfo = await ops.getLatestImage();
|
const imgInfo = await ops.getLatestImage();
|
||||||
console.log('imgInfo:', JSON.stringify(imgInfo, null, 2));
|
console.log('imgInfo:', JSON.stringify(imgInfo, null, 2));
|
||||||
|
|
||||||
if (imgInfo.ok && imgInfo.src) {
|
if (imgInfo.ok && imgInfo.src) {
|
||||||
console.log(`[5] 找到图片 (${imgInfo.width}x${imgInfo.height}, isNew=${imgInfo.isNew})`);
|
console.log(`[6] 找到图片 (${imgInfo.width}x${imgInfo.height}, isNew=${imgInfo.isNew})`);
|
||||||
|
|
||||||
// 提取 base64 数据
|
// 提取 base64 数据
|
||||||
console.log(`[5] 提取图片数据 (src=${imgInfo.src})...`);
|
console.log(`[6] 提取图片数据 (src=${imgInfo.src})...`);
|
||||||
const b64Result = await ops.extractImageBase64(imgInfo.src);
|
const b64Result = await ops.extractImageBase64(imgInfo.src);
|
||||||
|
|
||||||
if (b64Result.ok && b64Result.dataUrl) {
|
if (b64Result.ok && b64Result.dataUrl) {
|
||||||
@@ -160,7 +170,7 @@ async function main() {
|
|||||||
const base64Data = matches[2];
|
const base64Data = matches[2];
|
||||||
const buffer = Buffer.from(base64Data, 'base64');
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
// 保存到 ~/gemini-skill-output/
|
// 保存到 ./gemini-image/
|
||||||
const outputDir = './gemini-image';
|
const outputDir = './gemini-image';
|
||||||
if (!existsSync(outputDir)) {
|
if (!existsSync(outputDir)) {
|
||||||
mkdirSync(outputDir, { recursive: true });
|
mkdirSync(outputDir, { recursive: true });
|
||||||
@@ -169,15 +179,15 @@ async function main() {
|
|||||||
const filepath = join(outputDir, filename);
|
const filepath = join(outputDir, filename);
|
||||||
|
|
||||||
writeFileSync(filepath, buffer);
|
writeFileSync(filepath, buffer);
|
||||||
console.log(`[5] ✅ 图片已保存: ${filepath} (${(buffer.length / 1024).toFixed(1)} KB, method=${b64Result.method})`);
|
console.log(`[6] ✅ 图片已保存: ${filepath} (${(buffer.length / 1024).toFixed(1)} KB, method=${b64Result.method})`);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[5] ⚠ dataUrl 格式无法解析');
|
console.warn('[6] ⚠ dataUrl 格式无法解析');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[5] ⚠ 提取图片数据失败: ${b64Result.error || 'unknown'}`);
|
console.warn(`[6] ⚠ 提取图片数据失败: ${b64Result.error || 'unknown'}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[5] 未找到图片(可能本次回答不含图片)');
|
console.log('[6] 未找到图片(可能本次回答不含图片)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,17 @@ const SELECTORS = {
|
|||||||
'[data-test-id="overflow-container"]', // 测试专属属性
|
'[data-test-id="overflow-container"]', // 测试专属属性
|
||||||
'div.overflow-container', // class 兜底
|
'div.overflow-container', // class 兜底
|
||||||
],
|
],
|
||||||
|
/** 加号面板按钮(点击后弹出上传菜单) */
|
||||||
|
uploadPanelBtn: [
|
||||||
|
'button.upload-card-button[aria-haspopup="menu"]', // class + aria 组合
|
||||||
|
'button[aria-controls="upload-file-u"]', // aria-controls 兜底
|
||||||
|
'button.upload-card-button', // class 兜底
|
||||||
|
],
|
||||||
|
/** 上传文件选项(加号面板展开后的"上传文件"按钮) */
|
||||||
|
uploadFileBtn: [
|
||||||
|
'[data-test-id="uploader-images-files-button-advanced"]', // 测试专属属性
|
||||||
|
'images-files-uploader', // 标签名兜底
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -687,6 +698,65 @@ export function createOps(page) {
|
|||||||
|
|
||||||
// ─── 高层组合操作 ───
|
// ─── 高层组合操作 ───
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片到 Gemini 输入框
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 点击加号面板按钮,展开上传菜单
|
||||||
|
* 2. 等待 300ms 让菜单动画稳定
|
||||||
|
* 3. 拦截文件选择器 + 点击"上传文件"按钮(Promise.all 并发)
|
||||||
|
* 4. 向文件选择器塞入指定图片路径
|
||||||
|
* 5. 轮询等待图片加载完成(.image-preview.loading 消失)
|
||||||
|
*
|
||||||
|
* @param {string} filePath - 本地图片的绝对路径
|
||||||
|
* @returns {Promise<{ok: boolean, elapsed?: number, warning?: string, error?: string, detail?: string}>}
|
||||||
|
*/
|
||||||
|
async uploadImage(filePath) {
|
||||||
|
try {
|
||||||
|
// 1. 点击加号面板按钮,展开上传菜单
|
||||||
|
const panelClick = await this.click('uploadPanelBtn');
|
||||||
|
if (!panelClick.ok) {
|
||||||
|
return { ok: false, error: 'upload_panel_click_failed', detail: panelClick.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 等待菜单动画稳定
|
||||||
|
await sleep(250);
|
||||||
|
|
||||||
|
// 3. Promise.all 是精髓:一边开始监听文件选择器弹窗,一边点击"上传文件"按钮
|
||||||
|
const [fileChooser] = await Promise.all([
|
||||||
|
page.waitForFileChooser({ timeout: 3_000 }),
|
||||||
|
this.click('uploadFileBtn'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. 弹窗被拦截,塞入文件
|
||||||
|
await fileChooser.accept([filePath]);
|
||||||
|
console.log(`[ops] 文件已塞入,等待 Gemini 加载图片...`);
|
||||||
|
|
||||||
|
// 5. 等待图片加载完成(.image-preview.loading 消失)
|
||||||
|
const loadTimeout = 10_000;
|
||||||
|
const loadInterval = 250;
|
||||||
|
const loadStart = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - loadStart < loadTimeout) {
|
||||||
|
const loading = await op.query(() => {
|
||||||
|
const el = document.querySelector('.image-preview.loading');
|
||||||
|
return !!el;
|
||||||
|
});
|
||||||
|
if (!loading) {
|
||||||
|
console.log(`[ops] 图片加载完成 (${Date.now() - loadStart}ms): ${filePath}`);
|
||||||
|
return { ok: true, elapsed: Date.now() - loadStart };
|
||||||
|
}
|
||||||
|
await sleep(loadInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时了但文件已经塞进去了,不算完全失败
|
||||||
|
console.warn(`[ops] 图片加载超时 (${loadTimeout}ms),但文件已提交`);
|
||||||
|
return { ok: true, warning: 'load_timeout', elapsed: Date.now() - loadStart };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: 'upload_image_failed', detail: e.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送提示词并等待生成完成
|
* 发送提示词并等待生成完成
|
||||||
* @param {string} prompt
|
* @param {string} prompt
|
||||||
|
|||||||
Reference in New Issue
Block a user