'use strict'; const assert = require('assert'); const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); const { syncExistingAistudioApiKey } = require('../src/services/aistudio_api_key_service'); const { watchManualAistudioKey } = require('../src/services/aistudio_manual_key_watcher'); const dummyKey = `AIza${'A'.repeat(32)}`; const projectId = 'manual-project-25180'; (async () => { const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage({ viewport: { width: 1200, height: 800 } }); await page.route('https://aistudio.google.com/api-keys', route => route.fulfill({ status: 200, contentType: 'text/html', body: `
|
Manual Gemini Key
|
${projectId}
|
projects/568908549937
`; document.body.appendChild(dialog); }, { key: dummyKey }); const watched = await watchPromise; assert.strictEqual(watched.ok, true, '点击浏览器后应自动识别人工创建成功的 Key 详情'); assert.strictEqual(watched.data.apiKey, dummyKey, '自动监听应读取完整 Key'); const serviceSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'services', 'aistudio_api_key_service.js'), 'utf8'); const syncStart = serviceSource.indexOf('async function syncExistingAistudioApiKey'); const syncEnd = serviceSource.indexOf('\n/**', syncStart + 20); const syncSource = serviceSource.slice(syncStart, syncEnd); assert(!syncSource.includes('deleteAllKeys('), '人工同步流程不得删除线上 Key'); assert(!syncSource.includes('createKey('), '人工同步流程不得新建线上 Key'); const stepSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'pipeline', 'steps', 'aistudio_sync_key.js'), 'utf8'); assert(stepSource.includes("dbWrite('upsertAistudioApiKey'"), '同步成功必须写入 Key 数据库'); assert(stepSource.includes("dbWrite('addAistudioFlowLog'"), '同步成功必须追加 AI Studio 流程日志'); assert(stepSource.includes("'success'"), '人工同步日志必须标记为成功'); assert(stepSource.includes('aistudio_pending_project:'), '人工同步成功后必须清除该邮箱遗留的待同步 project ID'); const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8'); const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8'); assert(!html.includes('openAistudioKeysModal()'), '顶部总 API Keys 查看入口应移除'); assert(html.includes('openAistudioKeyModal(account)'), '账号列表行必须提供单邮箱查看入口'); assert(!html.includes('syncAistudioKey(account)'), '页面不应要求用户创建 Key 后再点一次“同步人工 Key”'); assert(app.includes("runTask('open_manual_aistudio'"), '点击浏览器必须同时启动人工 AI Studio Key 自动监听'); assert(app.includes('aistudio-api-keys?email=${encodeURIComponent(account.email)}'), '查看接口必须按当前邮箱查询,不能读取全部账号'); const runnerSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'workers', 'task_runner.js'), 'utf8'); assert(runnerSource.includes("taskType === 'aistudio_sync_key'"), '人工 Key 同步必须有独立的浏览器恢复策略'); assert(runnerSource.includes('aistudioSyncBrowserRetry < 1'), '比特浏览器被人工关闭/重开时应重连一次,但不能无限重试'); assert(runnerSource.includes('Target page, context or browser has been closed'), '只应对 page/context/browser 被关闭的瞬时错误执行重连'); assert(runnerSource.includes('isClosedBrowserError(lastFailure.msg)'), '登录服务将关闭错误转成普通步骤失败时,也必须触发一次重连'); assert(runnerSource.includes("case 'open_manual_aistudio':"), '工作器必须在比特浏览器打开后持续监听人工 Key'); assert(runnerSource.includes("dbWrite('upsertAistudioApiKey'"), '自动监听读到 Key 后必须直接写入当前邮箱数据库'); assert(runnerSource.includes('人工创建的 AI Studio API Key 已自动保存'), '自动保存后必须把 AI Studio 流程日志标记为成功'); const manualOpenStart = runnerSource.indexOf("case 'open_manual_aistudio':"); const manualOpenEnd = runnerSource.indexOf("\n case '", manualOpenStart + 20); const manualOpenSource = runnerSource.slice(manualOpenStart, manualOpenEnd); assert(manualOpenSource.includes('aistudio_pending_project:'), '浏览器人工监听保存成功后必须清除该邮箱遗留的待同步 project ID'); const operationsSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'routes', 'operations.js'), 'utf8'); assert(operationsSource.includes("taskType === 'open_manual_aistudio' && !isLocalRequest(req)"), '能读取人工 Key 的浏览器监听任务必须只允许本机启动'); assert(operationsSource.includes('Promise.resolve(worker.terminate())'), '自动保存完成后必须断开监听 Worker,但不能调用 browser.close() 关闭用户窗口'); console.log('aistudio_manual_key_sync.test.js: PASS'); } finally { await browser.close(); } })().catch(error => { console.error(error); process.exitCode = 1; });