const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const vm = require('vm'); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'account-credentials-')); const dbPath = path.join(tempDir, 'accounts.db'); const Config = require('../src/core/config'); Config.DATA_DIR = tempDir; Config.DB_PATH = dbPath; const database = require('../src/core/database'); const template = '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}----{{接码链接}}----{{rt凭证}}'; const importedLine = 'user@example.com----OldPassword----无辅助邮箱----abcdef234567----https://sms.example/session----1//OLD_REFRESH_TOKEN'; (async () => { try { database.initDB(); const result = database.importAccountsFromText(importedLine, '----', '未登录', template); assert.strictEqual(result.successCount, 1); assert.strictEqual(result.errorCount, 0); let account = database.getAccount('user@example.com'); assert.strictEqual(account.sms_url, 'https://sms.example/session', '导入模板应保存接码链接'); assert.strictEqual(account.ag_refresh_token, '1//OLD_REFRESH_TOKEN', '导入模板应保存 RT 凭证'); database.updateAccountPassword(account.email, 'NewPassword'); account = database.getAccount(account.email); assert.strictEqual(account.sms_url, 'https://sms.example/session', '修改密码不应清除接码链接'); assert.strictEqual(account.ag_refresh_token, '1//OLD_REFRESH_TOKEN', '修改密码不应清除已导入的 RT 凭证'); database.updateAccountAgToken(account.email, '1//NEW_REFRESH_TOKEN'); account = database.getAccount(account.email); assert.strictEqual(account.ag_refresh_token, '1//NEW_REFRESH_TOKEN', '新获取的 RT 凭证应覆盖导入值'); const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8'); let clipboardText = ''; const context = vm.createContext({ console, Set, Map, Date, JSON, Math, String, Number, Object, Array, RegExp, Promise, Uint8Array, setTimeout, clearTimeout, navigator: { clipboard: { writeText(text) { clipboardText = text; } } }, }); vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' }); const app = context.__geminiApp(); assert.strictEqual(app.deleteBrowserWindowsOnStage, false, '暂存时删除比特窗口必须默认关闭'); app.settings = { import_templates: JSON.stringify([ { name: '默认4字段(----)', pattern: '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}' }, ]), }; app.saveSetting = async (key, value) => { app.settings[key] = value; }; await app.loadImportTemplates(); assert.strictEqual( app.importTemplates[0].pattern, '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}----{{rt凭证}}', '已有配置升级后应自动补入默认5字段模板', ); assert.ok(app.importTemplates.some(item => item.pattern === template), '升级后仍应保留默认6字段模板'); app.importSelectedIndex = app.importTemplates.findIndex(item => item.pattern === template); app.importText = importedLine; const preview = app.getImportPreview()[0]; assert.deepStrictEqual( Array.from(preview, col => col.label), ['账号', '密码', '辅助邮箱', '2fa', '接码链接', 'rt凭证'], '导入预览应显示全部6个字段', ); const shiftedLine = 'shifted@example.com----Password----JBSWY3DPEHPK3PXP----1//SHIFTED_REFRESH_TOKEN'; const shiftedResult = database.importAccountsFromText(shiftedLine, '----', '未登录', template); assert.strictEqual(shiftedResult.successCount, 1); const shifted = database.getAccount('shifted@example.com'); assert.strictEqual(shifted.secret_key, 'jbswy3dpehpk3pxp', '应自动纠正被放入辅助邮箱列的2FA密钥'); assert.strictEqual(shifted.ag_refresh_token, '1//SHIFTED_REFRESH_TOKEN', '应自动纠正被放入2FA列的RT凭证'); const noSmsLine = 'nosms@example.com----Password--------JBSWY3DPEHPK3PXP----1//NO_SMS_REFRESH_TOKEN'; const noSmsResult = database.importAccountsFromText(noSmsLine, '----', '未登录', template); assert.strictEqual(noSmsResult.successCount, 1); const noSms = database.getAccount('nosms@example.com'); assert.strictEqual(noSms.secret_key, 'jbswy3dpehpk3pxp'); assert.strictEqual(noSms.sms_url, null, 'RT凭证不能留在接码链接字段'); assert.strictEqual(noSms.ag_refresh_token, '1//NO_SMS_REFRESH_TOKEN'); const fourFieldTemplate = '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}'; const swappedLine = 'swapped@example.com----Password----JBSWY3DPEHPK3PXP----recovery@example.com'; const swappedFourFieldResult = database.importAccountsFromText(swappedLine, '----', '未登录', fourFieldTemplate); assert.strictEqual(swappedFourFieldResult.successCount, 1); const swappedFourField = database.getAccount('swapped@example.com'); assert.strictEqual(swappedFourField.recovery_email, 'recovery@example.com', '第三列是2FA、第四列是辅助邮箱时应自动换位'); assert.strictEqual(swappedFourField.secret_key, 'jbswy3dpehpk3pxp'); const packedLine = 'packed@example.com----Password--------JBSWY3DPEHPK3PXP----1//PACKED_REFRESH_TOKEN'; const packedResult = database.importAccountsFromText(packedLine, '----', '未登录', fourFieldTemplate); assert.strictEqual(packedResult.successCount, 1); const packed = database.getAccount('packed@example.com'); assert.strictEqual(packed.recovery_email, null); assert.strictEqual(packed.secret_key, 'jbswy3dpehpk3pxp', '4字段模板应从最后一列拆出2FA'); assert.strictEqual(packed.ag_refresh_token, '1//PACKED_REFRESH_TOKEN', '4字段模板应保留后续RT凭证'); app.importSelectedIndex = app.importTemplates.findIndex(item => item.pattern === fourFieldTemplate); app.importText = swappedLine; const swappedPreview = app.getImportPreview()[0]; assert.strictEqual(swappedPreview.find(col => col.label === '辅助邮箱').value, 'recovery@example.com'); assert.strictEqual(swappedPreview.find(col => col.label === '2fa').value, 'JBSWY3DPEHPK3PXP'); app.importText = packedLine; const packedPreview = app.getImportPreview()[0]; assert.strictEqual(packedPreview.find(col => col.label === '2fa').value, 'JBSWY3DPEHPK3PXP'); assert.strictEqual(packedPreview.find(col => col.label === 'rt凭证').value, '1//PACKED_REFRESH_TOKEN'); const mixedSeparatorLine = 'mixed@example.com---Password---辅空---JBSW Y3DP EHPK 3PXP JBSW Y3DP EHPK 3PXP----1//MIXED_SEPARATOR_REFRESH_TOKEN'; const mixedSeparatorResult = database.importAccountsFromText(mixedSeparatorLine, '----', '未登录', fourFieldTemplate); assert.strictEqual(mixedSeparatorResult.successCount, 1, '应兼容3个和4个横线混用的分隔符'); const mixedSeparator = database.getAccount('mixed@example.com'); assert.strictEqual(mixedSeparator.password, 'Password'); assert.strictEqual(mixedSeparator.recovery_email, '辅空'); assert.strictEqual(mixedSeparator.secret_key.replace(/\s/g, ''), 'jbswy3dpehpk3pxpjbswy3dpehpk3pxp'); assert.strictEqual(mixedSeparator.ag_refresh_token, '1//MIXED_SEPARATOR_REFRESH_TOKEN'); app.importText = mixedSeparatorLine; const mixedSeparatorPreview = app.getImportPreview()[0]; assert.strictEqual(mixedSeparatorPreview.find(col => col.label === '账号').value, 'mixed@example.com'); assert.strictEqual(mixedSeparatorPreview.find(col => col.label === '密码').value, 'Password'); assert.strictEqual(mixedSeparatorPreview.find(col => col.label === '辅助邮箱').value, '辅空'); assert.ok(mixedSeparatorPreview.find(col => col.label === '2fa').value.includes('JBSW Y3DP')); assert.strictEqual(mixedSeparatorPreview.find(col => col.label === 'rt凭证').value, '1//MIXED_SEPARATOR_REFRESH_TOKEN'); app._exportList = [{ ...account, password: 'NewPassword', recovery_email: '无辅助邮箱', secret_key: 'abcdef234567' }]; Object.keys(app.exportFields).forEach(key => { app.exportFields[key] = false; }); ['email', 'password', 'recovery_email', 'secret_key', 'sms_url', 'ag_refresh_token'].forEach(key => { app.exportFields[key] = true; }); app.exportMode = { file: false, clipboard: true }; app.addLog = () => {}; await app.confirmExport(); assert.strictEqual( clipboardText, 'user@example.com----NewPassword----无辅助邮箱----abcdef234567----https://sms.example/session----1//NEW_REFRESH_TOKEN', '导出应按6字段顺序使用最新密码、原接码链接和最新 RT 凭证', ); let stagingPayload = null; context.fetch = async (url, options = {}) => { if (url === '/api/accounts/staging') { stagingPayload = JSON.parse(options.body); return { json: async () => ({ success: true, count: 1, batchId: 1, browserDeleteRequested: true, browsersFound: 0, browsersDeleted: 0, browserDeleteFailed: 0 }) }; } if (url === '/api/accounts') { return { json: async () => ({ success: true, data: [] }) }; } throw new Error(`未处理的测试请求: ${url}`); }; app.deleteBrowserWindowsOnStage = true; app.exportStagingNote = '测试文件夹'; app.selectedEmails = new Set([account.email]); await app.stageExportAccounts([account], ['email', 'password']); assert.strictEqual(stagingPayload.deleteBrowserWindows, true, '用户打开开关后应请求删除比特窗口'); console.log('account_credentials_import_export.test.js: PASS'); } finally { try { database.getDb().close(); } catch (_) {} fs.rmSync(tempDir, { recursive: true, force: true }); } })().catch(error => { console.error(error); process.exitCode = 1; });