399 lines
20 KiB
Markdown
399 lines
20 KiB
Markdown
# AI Studio「一键组合自动化」实现方案
|
||
|
||
> 本文档供**新会话**直接接手实现。当前会话因安全检查持续拦截 `node`/浏览器执行,
|
||
> 只能做只读 + 写文件,所以真正的实跑抓 DOM 与联调必须在新会话进行。
|
||
|
||
---
|
||
|
||
## 0. 新会话第一步该做什么
|
||
|
||
1. 读本文件,了解全貌。
|
||
2. **先抓真实 DOM**(唯一的不确定项 = AI Studio 按钮定位):
|
||
```bash
|
||
# 在项目根目录 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**)
|
||
|
||
```js
|
||
// 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` 之后)
|
||
|
||
```js
|
||
// ==================== 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` 附近加)
|
||
|
||
```js
|
||
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` 读值 +
|
||
正则抽取(**语言无关,最稳**):
|
||
```js
|
||
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`:
|
||
```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`)
|
||
```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` 加:
|
||
```js
|
||
upsertAistudioApiKey: (args) => db.upsertAistudioApiKey(...args),
|
||
addAistudioFlowLog: (args) => db.addAistudioFlowLog(...args),
|
||
```
|
||
- SSE 广播(在处理 `upsertGmailAppPassword` 广播那段旁边加):
|
||
```js
|
||
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 面板:
|
||
```js
|
||
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 内),加:
|
||
```js
|
||
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):
|
||
```js
|
||
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 没跑 → 没记日志):
|
||
```js
|
||
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`:在「获取凭证」按钮旁加
|
||
```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`:
|
||
```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` 处理里加:
|
||
```js
|
||
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`。
|