Files
go_bt/tools/aistudio_combo_plan.md
2026-09-24 10:57:31 +08:00

20 KiB
Raw Permalink Blame History

AI Studio「一键组合自动化」实现方案

本文档供新会话直接接手实现。当前会话因安全检查持续拦截 node/浏览器执行, 只能做只读 + 写文件,所以真正的实跑抓 DOM 与联调必须在新会话进行。


0. 新会话第一步该做什么

  1. 读本文件,了解全貌。
  2. 先抓真实 DOM(唯一的不确定项 = AI Studio 按钮定位):
    # 在项目根目录 D:\bt\go比特版本精简_打包\go比特版本精简 下
    node tools/dump_aistudio_dom.js <一个已登录Google的CloakBrowser环境ID>
    
    • 前置:CloakBrowser Manager 已启动(默认 127.0.0.1:8722);该环境内 Google 已登录。
    • 输出:tools/aistudio_dump/<时间戳>/,含 01_list / 02_row_menu / 03_create_dialog / 04_project_dropdown 的 .json + .html。
    • 只有加 --create 才会真建一枚 key 去抓 05_key_details(有副作用,仅测试账号用)。
    • 脚本默认不删除、不创建任何 key/项目。
  3. 读抓取结果,据真实 DOM 定稿第 4 步的选择器(替换第 6 节里标注「⚠️需实测确认」的选择器)。
  4. 按第 4~6 节实现全部改动,第 7 节自测。

1. 需求(用户明确要求)

一个按钮「AI Studio 一键组合」→ 对每个选中的 Google 账号:打开一次 CloakBrowser 窗口(用该账号自己的代理,4 步共用这一个窗口),顺序执行:

  1. 改密码
  2. 更改 2FA
  3. 获取凭证(含 16 位应用密码,复用现有 ag_get_credential,含 Antigravity OAuth 重新授权)
  4. 处理 Google AI Studio API Key(https://aistudio.google.com/api-keys)

失败即停:任一步失败立即跳过该账号、记为失败,并记录失败的步骤名; 只有全部步骤成功才记 success。

其它约束:

  • 新 key 明文存储即可(用户原话「数据库存储明文存储即可」)。
  • 新流程日志表必须与现有 operation_logs 并存且不影响它。
  • 语言无关定位:AI Studio 界面英/葡混用,禁止依赖文字匹配为主,用 role/aria + Material 图标 ligature + 对话框内位置 + 读值正则。
  • 所有对用户的回复用中文。

2. 第 4 步 AI Studio 的完整流程(已由 15 张截图确认)

进入 https://aistudio.google.com/api-keys,看表格有无已存在 key。

分支 A —— 已存在 key(先删后建)

  1. 逐行:点该行 ⋮(more_vert) → 菜单点「Delete key」(垃圾桶 delete 图标)
  2. 确认框「Delete API key?」→ 点危险动作按钮(对话框里非 Cancel 的那个 = 右侧/最后一个)
  3. 等 toast「Deleting API key...」消失、该行消失
  4. 循环直到列表清空(空态文案「Can't find your API keys here?」)
  5. 点右上「Create API key」→ 对话框里项目下拉已自动选中现有项目(如 "Default Gemini Project"),Create key 直接可点
  6. 点「Create key」→ 短暂「Loading...」→ 弹出「API key details」

分支 B —— 无 key(可能也无项目)

  1. 点「Create API key」→ 对话框
  2. 若下拉显示「No Cloud Projects Available」:展开下拉 → 点「Create project」(加号 add 图标) → 「Create a new project」对话框输入框默认「Gemini Project」→ 点「Create project」 → toast「New project created!」→ 创建 key 对话框下拉「Loading...」后自动选中新项目
  3. Name 用默认「Gemini API Key」,点「Create key」→「API key details」

收尾(两分支相同)—— 从「API key details」抓 4 个字段

  • API Key(示例 AQ.Ab8RN6L9PT1_3S8PJ9C4qnsF0nEBoo_IG1mAs7WUDyVTX4Ofbw)
  • Name(Gemini API Key)
  • Project name(projects/833454763400)
  • Project number(833454763400)

默认值:项目名 Gemini Project,key 名 Gemini API Key。 删除策略:删该邮箱下全部已有 key,再新建一枚。


3. 关键代码事实(已核对,实现时依赖)

  • 注册进 RECIPES 的 taskType 会自动走 pipeline:task_runner.js 里 PIPELINE_TASKS = new Set([...Object.keys(RECIPES), 'custom_pipeline']), 自动获得 runConcurrent → withBrowser → new PipelineContext → runSteps(ctx, recipe)。
  • step_runner.runSteps:某 step 抛异常 → 设 ctx.failure = { step: 函数名, msg, meta } 且 ctx.shouldStop = true。但 changePasswordStep/change2faStep 失败时只设 ctx.shouldStop、不设 ctx.failure,只在 ctx.results.changePassword/change2fa 里写 {success:false, msg}。→ 流程日志判定失败步骤必须按 ctx.results 顺序判断。
  • ag_get_credential 成功设 ctx.results.agGetCredential={success:true};失败设 ctx.results.agGetCredential={success:false,...} 且自己设 ctx.failure.step='agGetCredentialStep'。
  • Worker 内 DB 写入经 ctx.dbWrite('fnName', [args]) → parentPort 代理到主线程 operations.js 的 DB_FUNCTIONS[fnName](args)。新 db 函数必须注册进 DB_FUNCTIONS, 否则 worker 调不到。
  • 浏览器连接样板(browser_lifecycle.js): browser = await chromium.connectOverCDP(ws) → context = browser.contexts()[0] → page = await context.newPage()。
  • LOCAL_CREDENTIAL_TASKS(operations.js)内的 taskType 仅允许本机触发,且强制 JSON。
  • 自动换代理重试:task_runner.js 约 747 行 canRetry 判定。改密/改 2FA 不可重复执行, 故 aistudio_combo 必须像 security_chatgpt 一样排除出重试。
  • 参考样板文件:src/services/gmail_app_password_service.js(firstVisible / pageSnapshot / 读值正则,已在多语言 Google 页面跑通);src/pipeline/steps/ag_get_credential.js(step 结构)。

4. 数据库改动(src/core/database.js)—— 已确认可行,代码如下

4.1 两张新表(放在 gmail_app_passwords 建表附近;不动 operation_logs)

// Google AI Studio 生成的 API Key 独立保存,一个邮箱只保留当前最新一枚。
// 按用户要求 api_key 明文存储;raw_json 保存对话框抓到的 4 字段原文,供前端 JSON 展示。
db.exec(`
  CREATE TABLE IF NOT EXISTS aistudio_api_keys (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE COLLATE NOCASE,
    api_key TEXT,
    key_name TEXT,
    project_name TEXT,
    project_number TEXT,
    raw_json TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

// AI Studio 一键组合流程专用日志表。任一步失败记 failed + 失败步骤名;全部成功记 success。
// 与 operation_logs 并存、互不影响。
db.exec(`
  CREATE TABLE IF NOT EXISTS aistudio_flow_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT,
    status TEXT NOT NULL,
    failed_step TEXT,
    detail TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

4.2 新函数(放在 hasGmailAppPassword 之后)

// ==================== AI Studio API Key ====================
function upsertAistudioApiKey(email, data) {
  const normalizedEmail = String(email || '').trim();
  if (!normalizedEmail) throw new Error('邮箱不能为空');
  const d = data && typeof data === 'object' ? data : {};
  const apiKey = d.apiKey != null ? String(d.apiKey) : '';
  const keyName = d.keyName != null ? String(d.keyName) : '';
  const projectName = d.projectName != null ? String(d.projectName) : '';
  const projectNumber = d.projectNumber != null ? String(d.projectNumber) : '';
  const rawJson = JSON.stringify({ email: normalizedEmail, apiKey, keyName, projectName, projectNumber });
  getDb().prepare(`
    INSERT INTO aistudio_api_keys (email, api_key, key_name, project_name, project_number, raw_json, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
    ON CONFLICT(email) DO UPDATE SET
      api_key = excluded.api_key, key_name = excluded.key_name,
      project_name = excluded.project_name, project_number = excluded.project_number,
      raw_json = excluded.raw_json, updated_at = CURRENT_TIMESTAMP
  `).run(normalizedEmail, apiKey, keyName, projectName, projectNumber, rawJson);
  return true;
}
function getAistudioApiKey(email) {
  const e = String(email || '').trim();
  if (!e) return null;
  return getDb().prepare('SELECT * FROM aistudio_api_keys WHERE email = ? COLLATE NOCASE').get(e) || null;
}
function getAllAistudioApiKeys() {
  return getDb().prepare('SELECT * FROM aistudio_api_keys ORDER BY updated_at DESC, id DESC').all();
}
function addAistudioFlowLog(email, status, failedStep, detail) {
  getDb().prepare(`
    INSERT INTO aistudio_flow_logs (email, status, failed_step, detail) VALUES (?, ?, ?, ?)
  `).run(
    String(email || '').trim() || null,
    String(status || '').trim() || 'unknown',
    failedStep ? String(failedStep) : null,
    detail ? String(detail) : null,
  );
}
function getAistudioFlowLogs(limit = 200) {
  const n = Math.max(1, Math.min(2000, parseInt(limit, 10) || 200));
  return getDb().prepare('SELECT * FROM aistudio_flow_logs ORDER BY id DESC LIMIT ?').all(n);
}

4.3 导出(module.exports 里,hasGmailAppPassword 附近加)

upsertAistudioApiKey,
getAistudioApiKey,
getAllAistudioApiKeys,
addAistudioFlowLog,
getAistudioFlowLogs,

5. 服务(新建 src/services/aistudio_api_key_service.js)

对外导出 processAistudioApiKey(page, account, log, options): 返回 { ok, data: {apiKey,keyName,projectName,projectNumber}, msg }。

内部结构(照抄 gmail_app_password_service.js 的 firstVisible / pageSnapshot 工具):

  1. gotoApiKeys(page):page.goto(API_KEYS_URL, {waitUntil:'domcontentloaded'}), 等 Angular 首屏(轮询直到出现「Create API key」按钮或表格,带超时)。 若被重定向到登录页 → 复用 login_service 的 ensureLoggedIn/continueGoogleVerification 重新验证(AI Studio 一般已复用 Google 登录态,多数无需)。
  2. countExistingKeys(page):数每行的 ⋮(more_vert) 按钮个数(⚠️需实测确认选择器)。
  3. deleteAllKeys(page):while 有行(带 max 次数护栏,如 50):点第一个 ⋮ → 菜单点含 delete 图标项 → 确认框点危险动作按钮(对话框内最后一个/右侧按钮)→ 等行数减少 / toast 消失。
  4. createKey(page, {projectName, keyName}):
    • 点「Create API key」→ 出现创建对话框。
    • 填 name 输入框(对话框内第一个 text input)= keyName。
    • 判断 Create key 按钮(对话框内最后一个按钮)是否 disabled:
      • 不 disabled → 直接点。
      • disabled → 展开项目下拉:
        • 若有真实项目选项(非 Import/Create project 动作行、非「No Cloud Projects」占位) → 选第一个真实项目。
        • 否则点「Create project」(加号 add 图标) → 项目对话框填名(默认 Gemini Project) → 点确认(对话框内最后一个按钮)→ 等回到创建 key 对话框且 Create key 变可点。
      • 再点 Create key。
  5. readKeyDetails(page):等「API key details」对话框出现,pageSnapshot 读值 + 正则抽取(语言无关,最稳):
    apiKey        = text.match(/AQ\.[A-Za-z0-9_\-]{16,}/)  || text.match(/AIza[A-Za-z0-9_\-]{20,}/)
    projectName   = text.match(/projects\/[A-Za-z0-9_\-]+/)
    projectNumber = projectName 里的 \d{4,}  ||  text.match(/\b\d{6,}\b/)
    keyName       = 取我们填入的默认值 'Gemini API Key'(详情框里 name 也可校验)
    
    成功判据:apiKey 非空。抓到后关闭对话框(点关闭/Copy 无所谓)。
  6. processAistudioApiKey:串起 goto → count → (删/不删) → create → readDetails, 每步 try/catch,失败返回 {ok:false, msg},不抛。

⚠️需实测确认的选择器(用第 0 节抓取结果替换):

  • 「Create API key」工具栏按钮
  • 每行 ⋮(more_vert) 按钮、菜单里的「Delete key」项
  • 删除确认框的危险动作按钮(确认「右侧/最后一个按钮 = 确认删除」这一假设)
  • 创建对话框的 name 输入框、项目下拉(mat-select/[role="combobox"])、Create key 按钮
  • 下拉里「Create project」动作行 vs 真实项目行的区分(图标 ligature)
  • 「Create a new project」对话框输入框 + 确认按钮
  • 「API key details」4 字段是 input 还是 span(决定 pageSnapshot 读法)

6. 其余改动

6.1 step(新建 src/pipeline/steps/aistudio_api_key.js)

仿 ag_get_credential.js:

async function aistudioApiKeyStep(ctx) {
  const { account, page, log, dbWrite, updateMessage } = ctx;
  await updateMessage(account.email, '正在处理 AI Studio API Key...');
  const res = await processAistudioApiKey(page, account, log, {
    projectName: 'Gemini Project', keyName: 'Gemini API Key',
  });
  if (res.ok && res.data && res.data.apiKey) {
    await dbWrite('upsertAistudioApiKey', [account.email, res.data]);
    ctx.results.aistudioApiKey = { success: true, data: res.data };
    await updateMessage(account.email, '✅ AI Studio API Key 已生成');
  } else {
    ctx.results.aistudioApiKey = { success: false, msg: res.msg || 'API Key 处理失败' };
    ctx.failure = { step: 'aistudioApiKeyStep', msg: res.msg || 'API Key 处理失败', meta: null };
    ctx.shouldStop = true;
    // 存错误截图(照抄 ag_get_credential 的 saveErrorScreenshot 用法)
  }
}
module.exports = aistudioApiKeyStep;

6.2 recipes(src/pipeline/recipes.js)

const aistudioApiKeyStep = require('./steps/aistudio_api_key');
// RECIPES 里加:
aistudio_combo: [loginStep, countryStep, changeLanguageStep, changePasswordStep, change2faStep, agGetCredentialStep, aistudioApiKeyStep],
// ATOM_STEPS 里加(可选,供自定义流水线复用):
aistudio_api_key: { step: aistudioApiKeyStep, label: 'AI Studio API Key' },

6.3 operations.js

  • DB_FUNCTIONS 加:
    upsertAistudioApiKey: (args) => db.upsertAistudioApiKey(...args),
    addAistudioFlowLog: (args) => db.addAistudioFlowLog(...args),
    
  • SSE 广播(在处理 upsertGmailAppPassword 广播那段旁边加):
    if (!error && msg.fn === 'upsertAistudioApiKey' && msg.args && msg.args[0]) {
      const saved = db.getAistudioApiKey(msg.args[0]);
      sseBroadcast(taskId, {
        type: 'account_update', email: msg.args[0],
        aistudio_api_key: saved ? saved.api_key : null,
        aistudio_api_key_json: saved ? (saved.raw_json || null) : null,
      });
    }
    
  • LOCAL_CREDENTIAL_TASKS 加 'aistudio_combo'(凭证类任务,仅本机 + 强制 JSON)。
  • 新增只读接口给前端 JSON 面板:
    router.get('/aistudio-api-keys', (req, res) => {
      res.json({ success: true, data: db.getAllAistudioApiKeys() });
    });
    router.get('/aistudio-flow-logs', (req, res) => {
      res.json({ success: true, data: db.getAistudioFlowLogs(Number(req.query.limit) || 200) });
    });
    

6.4 task_runner.js(写流程日志 + 排除重试)

在 pipeline 分支 await runSteps(ctx, recipe) 之后(withBrowser 的 fn 内),加:

if (taskType === 'aistudio_combo') {
  aistudioFlowHandled = true; // 在账号处理作用域先 let aistudioFlowHandled = false
  const failed = resolveAistudioFailedStep(ctx); // 见下
  if (failed) {
    await dbWrite('addAistudioFlowLog', [acc.email, 'failed', failed.label, failed.msg]).catch(()=>{});
  } else {
    await dbWrite('addAistudioFlowLog', [acc.email, 'success', '', '全部步骤成功']).catch(()=>{});
  }
}

模块级 helper(失败步骤按 ctx.results 顺序判定,兼容不设 ctx.failure 的改密/2FA):

const AISTUDIO_STEP_LABELS = {
  loginStep:'登录', countryStep:'查看国家', detectCountryStep:'查看国家',
  changeLanguageStep:'更改语言', changePasswordStep:'改密码', change2faStep:'更改2FA',
  agGetCredentialStep:'获取凭证', aistudioApiKeyStep:'处理AI Studio API Key',
};
function resolveAistudioFailedStep(ctx) {
  const r = (ctx && ctx.results) || {};
  if (r.login && r.login.success === false) return { label:'登录', msg:r.login.msg||'登录失败' };
  if (r.changePassword && r.changePassword.success === false) return { label:'改密码', msg:r.changePassword.msg||'改密失败' };
  if (r.change2fa && r.change2fa.success === false) return { label:'更改2FA', msg:r.change2fa.msg||'更改2FA失败' };
  if (r.agGetCredential && r.agGetCredential.success === false) return { label:'获取凭证', msg:r.agGetCredential.msg||'获取凭证失败' };
  if (r.aistudioApiKey && r.aistudioApiKey.success === false) return { label:'处理AI Studio API Key', msg:r.aistudioApiKey.msg||'API Key处理失败' };
  if (ctx.failure) return { label: AISTUDIO_STEP_LABELS[ctx.failure.step]||ctx.failure.step||'未知步骤', msg: ctx.failure.msg||'步骤异常' };
  if (ctx.shouldStop) return { label:'未知步骤', msg:'流程中断' };
  return null;
}
  • 循环外补漏(浏览器没打开成 → fn 没跑 → 没记日志):
    if (taskType === 'aistudio_combo' && !aistudioFlowHandled) {
      await dbWrite('addAistudioFlowLog', [acc.email, 'failed', '打开浏览器', (lastFailure&&lastFailure.msg)||'浏览器打开失败']).catch(()=>{});
    }
    
  • 约 747 行 canRetry:把 taskType !== 'security_chatgpt' 改成 taskType !== 'security_chatgpt' && taskType !== 'aistudio_combo'(避免改密/2FA 被重跑)。

6.5 前端

  • public/index.html:在「获取凭证」按钮旁加
    <button @click="runAistudioCombo()" :disabled="isRunning" class="btn btn-purple">AI Studio 一键组合</button>
    <button @click="openAistudioKeysModal()" class="btn btn-green">查看API Keys(JSON)</button>
    
    再加一个弹窗,<pre x-text="JSON.stringify(aistudioKeys, null, 2)">。
  • public/app.js:
    runAistudioCombo() { this.runTask('aistudio_combo'); },
    async openAistudioKeysModal() {
      const r = await fetch('/api/operations/aistudio-api-keys').then(x=>x.json());
      this.aistudioKeys = (r && r.data) || [];
      this.showAistudioKeysModal = true;
    },
    
    SSE account_update 处理里加:
    if (data.aistudio_api_key !== undefined) account.aistudio_api_key = data.aistudio_api_key;
    if (data.aistudio_api_key_json !== undefined) account.aistudio_api_key_json = data.aistudio_api_key_json;
    
    以及 _taskDisplayLabel / _taskPendingMessage 里给 aistudio_combo 加中文标签 「AI Studio 一键组合」/「等待处理 AI Studio...」。

7. 自测清单(新会话实现后)

  1. node -e "require('./src/core/database').initDB()" 建表不报错;.tables 看到两新表。
  2. 选一个测试账号点按钮 → 观察一个窗口内顺序跑完 4 步。
  3. 制造失败(如故意断网)→ aistudio_flow_logs 记 failed + 正确失败步骤名; 成功账号记 success。确认 operation_logs 行为不受影响。
  4. aistudio_api_keys 有一条明文记录;前端 JSON 面板能看到 4 字段。
  5. 已存在 key 的账号:确认先删干净再建一枚(列表最终只 1 枚)。
  6. 无项目账号:确认自动建「Gemini Project」再建 key。

8. 已完成 / 未完成

  • ✅ 已写:tools/dump_aistudio_dom.js(DOM 抓取脚本)、本方案文档。
  • ⛔ 未写任何功能代码(database.js 上一轮的改动已全部还原,仓库干净)。
  • 待新会话:先抓 DOM 定稿选择器,再按第 4~6 节实现,第 7 节自测。

9. 环境备忘

  • 端口 3001;启动 node server.js。
  • CloakBrowser Manager 本地 API 默认 127.0.0.1:8722;openBrowser 返回 data.ws(CDP 端点)。
  • 数据库 data/accounts.db。
  • 参考样板:src/services/gmail_app_password_service.js、src/pipeline/steps/ag_get_credential.js。