@lightharu/krouter 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +679 -0
- package/README.md +238 -0
- package/dist-web/assets/index-CM4-0adf.css +1 -0
- package/dist-web/assets/index-DCslvfUR.js +139 -0
- package/dist-web/favicon.svg +9 -0
- package/dist-web/icon.svg +9 -0
- package/dist-web/index.html +19 -0
- package/out-server/main/kiroAuthSync.js +249 -0
- package/out-server/main/kproxy/certManager.js +262 -0
- package/out-server/main/kproxy/index.js +254 -0
- package/out-server/main/kproxy/mitmProxy.js +475 -0
- package/out-server/main/kproxy/types.js +23 -0
- package/out-server/main/proxy/accountPool.js +543 -0
- package/out-server/main/proxy/clientConfig.js +596 -0
- package/out-server/main/proxy/index.js +25 -0
- package/out-server/main/proxy/kiroApi.js +1996 -0
- package/out-server/main/proxy/logger.js +407 -0
- package/out-server/main/proxy/modelCatalog.js +75 -0
- package/out-server/main/proxy/promptCacheTracker.js +301 -0
- package/out-server/main/proxy/proxyServer.js +3543 -0
- package/out-server/main/proxy/selfSignedCert.js +179 -0
- package/out-server/main/proxy/systemProxy.js +250 -0
- package/out-server/main/proxy/tokenCounter.js +164 -0
- package/out-server/main/proxy/toolNameRegistry.js +57 -0
- package/out-server/main/proxy/translator.js +1084 -0
- package/out-server/main/proxy/types.js +3 -0
- package/out-server/main/registration/browser-identity.js +184 -0
- package/out-server/main/registration/chainProxy.js +349 -0
- package/out-server/main/registration/config.js +58 -0
- package/out-server/main/registration/email-service.js +801 -0
- package/out-server/main/registration/fingerprint.js +352 -0
- package/out-server/main/registration/http-utils.js +148 -0
- package/out-server/main/registration/jwe.js +74 -0
- package/out-server/main/registration/names.js +142 -0
- package/out-server/main/registration/proton-mail-window.js +339 -0
- package/out-server/main/registration/registrar.js +1715 -0
- package/out-server/main/registration/tlsClientPool.js +70 -0
- package/out-server/main/registration/xxtea.js +161 -0
- package/out-server/main/runtimePaths.js +19 -0
- package/out-server/main/utils/redact.js +95 -0
- package/out-server/server/index.js +1272 -0
- package/out-server/server/services/accountExtras.js +105 -0
- package/out-server/server/services/accountProfileHydration.js +95 -0
- package/out-server/server/services/authFlows.js +509 -0
- package/out-server/server/services/dashboardTunnel.js +315 -0
- package/out-server/server/services/diagnostics.js +326 -0
- package/out-server/server/services/kiroAccounts.js +431 -0
- package/out-server/server/services/kiroSettings.js +260 -0
- package/out-server/server/services/kproxyRuntime.js +264 -0
- package/out-server/server/services/localKiroCredentials.js +320 -0
- package/out-server/server/services/machineIdRuntime.js +327 -0
- package/out-server/server/services/protonBrowserRuntime.js +724 -0
- package/out-server/server/services/proxyRuntime.js +523 -0
- package/out-server/server/services/registrationRuntime.js +106 -0
- package/out-server/server/store.js +266 -0
- package/package.json +113 -0
- package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
- package/scripts/kiro-manager-cli.cjs +3 -0
- package/scripts/krouter-cli.cjs +509 -0
- package/src/renderer/src/assets/krouter-logo.svg +11 -0
- package/src/renderer/src/assets/krouter-mark.svg +9 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 共享英文姓名库与随机生成逻辑
|
|
3
|
+
// 用于注册时生成更自然、低重复率的「全名」与「邮箱前缀」。
|
|
4
|
+
// 名字主体覆盖大量常见英文名/姓,邮箱前缀模拟真实用户的命名习惯,避免一眼机器生成。
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NICKNAMES = exports.LAST_NAMES = exports.FIRST_NAMES = void 0;
|
|
7
|
+
exports.randomFullName = randomFullName;
|
|
8
|
+
exports.randomEmailPrefix = randomEmailPrefix;
|
|
9
|
+
exports.FIRST_NAMES = [
|
|
10
|
+
// 男性常见名
|
|
11
|
+
'James', 'Robert', 'John', 'Michael', 'David', 'William', 'Richard', 'Joseph', 'Thomas', 'Charles',
|
|
12
|
+
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
|
|
13
|
+
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
|
|
14
|
+
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
|
|
15
|
+
'Benjamin', 'Samuel', 'Raymond', 'Gregory', 'Frank', 'Alexander', 'Patrick', 'Jack', 'Dennis', 'Jerry',
|
|
16
|
+
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Zachary', 'Douglas', 'Peter', 'Kyle',
|
|
17
|
+
'Noah', 'Ethan', 'Jeremy', 'Walter', 'Christian', 'Keith', 'Roger', 'Terry', 'Austin', 'Sean',
|
|
18
|
+
'Gerald', 'Carl', 'Harold', 'Dylan', 'Arthur', 'Lawrence', 'Jordan', 'Jesse', 'Bryan', 'Billy',
|
|
19
|
+
'Bruce', 'Gabriel', 'Joe', 'Logan', 'Alan', 'Juan', 'Albert', 'Elijah', 'Wayne', 'Randy',
|
|
20
|
+
'Vincent', 'Mason', 'Roy', 'Ralph', 'Russell', 'Bradley', 'Philip', 'Eugene', 'Louis', 'Caleb',
|
|
21
|
+
'Hunter', 'Connor', 'Aidan', 'Ian', 'Cameron', 'Owen', 'Luke', 'Isaac', 'Wesley', 'Carlos',
|
|
22
|
+
'Miguel', 'Antonio', 'Victor', 'Marcus', 'Travis', 'Cole', 'Blake', 'Shawn', 'Trevor', 'Spencer',
|
|
23
|
+
'Devin', 'Colin', 'Drew', 'Grant', 'Theodore', 'Oliver', 'Liam', 'Lucas', 'Nathaniel', 'Adrian',
|
|
24
|
+
'Dean', 'Derek', 'Evan', 'Fred', 'Harry', 'Hayden', 'Leo', 'Brad',
|
|
25
|
+
// 女性常见名
|
|
26
|
+
'Mary', 'Patricia', 'Jennifer', 'Linda', 'Barbara', 'Elizabeth', 'Susan', 'Jessica', 'Sarah', 'Karen',
|
|
27
|
+
'Lisa', 'Nancy', 'Betty', 'Margaret', 'Sandra', 'Ashley', 'Dorothy', 'Kimberly', 'Emily', 'Donna',
|
|
28
|
+
'Michelle', 'Carol', 'Amanda', 'Melissa', 'Deborah', 'Stephanie', 'Rebecca', 'Sharon', 'Laura', 'Cynthia',
|
|
29
|
+
'Kathleen', 'Amy', 'Angela', 'Shirley', 'Anna', 'Brenda', 'Pamela', 'Emma', 'Nicole', 'Helen',
|
|
30
|
+
'Samantha', 'Katherine', 'Christine', 'Debra', 'Rachel', 'Carolyn', 'Janet', 'Catherine', 'Maria', 'Heather',
|
|
31
|
+
'Diane', 'Olivia', 'Julie', 'Joyce', 'Victoria', 'Kelly', 'Christina', 'Joan', 'Evelyn', 'Lauren',
|
|
32
|
+
'Judith', 'Megan', 'Cheryl', 'Andrea', 'Hannah', 'Martha', 'Jacqueline', 'Frances', 'Gloria', 'Ann',
|
|
33
|
+
'Teresa', 'Kathryn', 'Sophia', 'Madison', 'Abigail', 'Grace', 'Natalie', 'Brittany', 'Danielle', 'Sara',
|
|
34
|
+
'Alexis', 'Isabella', 'Mia', 'Charlotte', 'Amelia', 'Ava', 'Chloe', 'Ella', 'Avery', 'Sofia',
|
|
35
|
+
'Aria', 'Scarlett', 'Allison', 'Audrey', 'Brooke', 'Claire', 'Lily', 'Zoe', 'Leah', 'Hailey',
|
|
36
|
+
'Paige', 'Vanessa', 'Alice', 'Amber', 'Aubrey', 'Beverly', 'Dawn', 'Diana', 'Holly', 'Julia',
|
|
37
|
+
'Kayla', 'Lucy', 'Lydia', 'Molly', 'Nora', 'Riley', 'Tammy', 'Tina', 'Valerie', 'Wendy'
|
|
38
|
+
];
|
|
39
|
+
exports.LAST_NAMES = [
|
|
40
|
+
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
|
|
41
|
+
'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin',
|
|
42
|
+
'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson',
|
|
43
|
+
'Walker', 'Young', 'Allen', 'King', 'Wright', 'Scott', 'Torres', 'Nguyen', 'Hill', 'Flores',
|
|
44
|
+
'Green', 'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera', 'Campbell', 'Mitchell', 'Carter', 'Roberts',
|
|
45
|
+
'Gomez', 'Phillips', 'Evans', 'Turner', 'Diaz', 'Parker', 'Cruz', 'Edwards', 'Collins', 'Reyes',
|
|
46
|
+
'Stewart', 'Morris', 'Morales', 'Murphy', 'Cook', 'Rogers', 'Gutierrez', 'Ortiz', 'Morgan', 'Cooper',
|
|
47
|
+
'Peterson', 'Bailey', 'Reed', 'Kelly', 'Howard', 'Ramos', 'Kim', 'Cox', 'Ward', 'Richardson',
|
|
48
|
+
'Watson', 'Brooks', 'Chavez', 'Wood', 'James', 'Bennett', 'Gray', 'Mendoza', 'Ruiz', 'Hughes',
|
|
49
|
+
'Price', 'Alvarez', 'Castillo', 'Sanders', 'Patel', 'Myers', 'Long', 'Ross', 'Foster', 'Jimenez',
|
|
50
|
+
'Powell', 'Jenkins', 'Perry', 'Russell', 'Sullivan', 'Bell', 'Coleman', 'Butler', 'Henderson', 'Barnes',
|
|
51
|
+
'Gonzales', 'Fisher', 'Vasquez', 'Simmons', 'Romero', 'Jordan', 'Patterson', 'Alexander', 'Hamilton', 'Graham',
|
|
52
|
+
'Reynolds', 'Griffin', 'Wallace', 'Moreno', 'West', 'Cole', 'Hayes', 'Bryant', 'Herrera', 'Gibson',
|
|
53
|
+
'Ellis', 'Tran', 'Medina', 'Aguilar', 'Stevens', 'Murray', 'Ford', 'Castro', 'Marshall', 'Owens',
|
|
54
|
+
'Harrison', 'Fernandez', 'Mcdonald', 'Woods', 'Washington', 'Kennedy', 'Wells', 'Vargas', 'Henry', 'Chen',
|
|
55
|
+
'Freeman', 'Webb', 'Tucker', 'Guzman', 'Burns', 'Crawford', 'Olson', 'Simpson', 'Porter', 'Hunter',
|
|
56
|
+
'Gordon', 'Mendez', 'Silva', 'Shaw', 'Snyder', 'Mason', 'Dixon', 'Munoz', 'Hunt', 'Hicks',
|
|
57
|
+
'Holmes', 'Palmer', 'Wagner', 'Black', 'Robertson', 'Boyd', 'Rose', 'Stone', 'Salazar', 'Fox',
|
|
58
|
+
'Warren', 'Mills', 'Meyer', 'Rice', 'Schmidt', 'Garza', 'Daniels', 'Ferguson', 'Nichols', 'Stephens',
|
|
59
|
+
'Soto', 'Weaver', 'Ryan', 'Gardner', 'Payne', 'Grant', 'Dunn', 'Kelley', 'Spencer', 'Hawkins',
|
|
60
|
+
'Arnold', 'Pierce', 'Vazquez', 'Hansen', 'Peters', 'Santos', 'Hart'
|
|
61
|
+
];
|
|
62
|
+
// 常见英文昵称(小写),仅用于邮箱前缀,模拟真人随意取名
|
|
63
|
+
exports.NICKNAMES = [
|
|
64
|
+
'mike', 'dave', 'chris', 'alex', 'sam', 'jess', 'kate', 'tom', 'nick', 'joe',
|
|
65
|
+
'dan', 'matt', 'rob', 'will', 'ben', 'jen', 'liz', 'beth', 'andy', 'tony',
|
|
66
|
+
'jim', 'bob', 'rick', 'steve', 'greg', 'ken', 'charlie', 'jack', 'jake', 'max',
|
|
67
|
+
'gabe', 'nate', 'zach', 'josh', 'tim', 'pat', 'vince', 'leo', 'ray', 'gene',
|
|
68
|
+
'marty', 'phil', 'pete', 'randy', 'russ', 'abby', 'allie', 'becky', 'bella', 'cassie',
|
|
69
|
+
'cathy', 'debbie', 'ellie', 'gabby', 'gracie', 'izzy', 'josie', 'katie', 'lucy', 'maggie',
|
|
70
|
+
'mandy', 'meg', 'mel', 'millie', 'nina', 'patty', 'penny', 'rosie', 'sadie', 'sally',
|
|
71
|
+
'sandy', 'sue', 'tess', 'val', 'vicky', 'wendy'
|
|
72
|
+
];
|
|
73
|
+
function randInt(max) {
|
|
74
|
+
return Math.floor(Math.random() * max);
|
|
75
|
+
}
|
|
76
|
+
function pick(arr) {
|
|
77
|
+
return arr[randInt(arr.length)];
|
|
78
|
+
}
|
|
79
|
+
// 少量随机小写字母后缀(1-2 个),仅在基础名字组合时用于补足唯一性
|
|
80
|
+
function randomLetters() {
|
|
81
|
+
const n = 1 + randInt(2);
|
|
82
|
+
let s = '';
|
|
83
|
+
for (let i = 0; i < n; i++)
|
|
84
|
+
s += String.fromCharCode(97 + randInt(26));
|
|
85
|
+
return s;
|
|
86
|
+
}
|
|
87
|
+
// 随机全名(用于注册显示名),偶尔带中间名首字母,进一步降低重复率
|
|
88
|
+
function randomFullName() {
|
|
89
|
+
const first = pick(exports.FIRST_NAMES);
|
|
90
|
+
const last = pick(exports.LAST_NAMES);
|
|
91
|
+
if (Math.random() < 0.18) {
|
|
92
|
+
const mid = String.fromCharCode(65 + randInt(26)); // A-Z
|
|
93
|
+
return `${first} ${mid}. ${last}`;
|
|
94
|
+
}
|
|
95
|
+
return `${first} ${last}`;
|
|
96
|
+
}
|
|
97
|
+
// 随机邮箱前缀:以真实名字成分组合为主(中间名、双姓等,无数字无乱码、最像真人),
|
|
98
|
+
// 少量基础组合补 1-2 个随机字母保证唯一,整体低重复且自然
|
|
99
|
+
function randomEmailPrefix() {
|
|
100
|
+
const first = pick(exports.FIRST_NAMES).toLowerCase();
|
|
101
|
+
const last = pick(exports.LAST_NAMES).toLowerCase();
|
|
102
|
+
const middle = pick(exports.FIRST_NAMES).toLowerCase();
|
|
103
|
+
const last2 = pick(exports.LAST_NAMES).toLowerCase();
|
|
104
|
+
const nick = pick(exports.NICKNAMES);
|
|
105
|
+
const fi = first.charAt(0);
|
|
106
|
+
const mi = middle.charAt(0);
|
|
107
|
+
const li = last.charAt(0);
|
|
108
|
+
const r = Math.random();
|
|
109
|
+
// 约 72%:真实名字多成分组合,高度唯一且最自然
|
|
110
|
+
if (r < 0.72) {
|
|
111
|
+
const s = pick(['.', '.', '.', '_']);
|
|
112
|
+
return pick([
|
|
113
|
+
`${first}${s}${middle}${s}${last}`, // john.michael.smith
|
|
114
|
+
`${first}${s}${mi}${s}${last}`, // john.m.smith
|
|
115
|
+
`${first}${mi}${s}${last}`, // johnm.smith
|
|
116
|
+
`${first}${s}${last}${s}${last2}`, // john.smith.brown(双姓)
|
|
117
|
+
`${fi}${s}${middle}${s}${last}`, // j.michael.smith
|
|
118
|
+
`${first}${s}${middle}`, // john.michael
|
|
119
|
+
`${middle}${s}${last}`, // michael.smith
|
|
120
|
+
`${nick}${s}${middle}${s}${last}` // mike.john.smith
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
// 约 18%:基础名字组合 + 1-2 个随机字母,兼顾自然与唯一
|
|
124
|
+
if (r < 0.9) {
|
|
125
|
+
const base = pick([
|
|
126
|
+
`${first}${last}`,
|
|
127
|
+
`${first}.${last}`,
|
|
128
|
+
`${fi}${last}`,
|
|
129
|
+
`${first}${li}`,
|
|
130
|
+
`${nick}${last}`,
|
|
131
|
+
`${last}${fi}`
|
|
132
|
+
]);
|
|
133
|
+
return `${base}${randomLetters()}`;
|
|
134
|
+
}
|
|
135
|
+
// 约 10%:纯净名字组合(无任何后缀),保留少量最简洁写法
|
|
136
|
+
return pick([
|
|
137
|
+
`${first}.${last}`,
|
|
138
|
+
`${first}${last}`,
|
|
139
|
+
`${nick}.${last}`,
|
|
140
|
+
`${first}.${middle}.${last}`
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Proton 邮箱「借壳官方网页」取码(轻量版,借鉴 ElectronMail 思路)
|
|
3
|
+
//
|
|
4
|
+
// 原理:ProtonMail 是 E2E 加密、无官方 IMAP/SMTP/公开 API。本模块在主进程起一个
|
|
5
|
+
// 带持久化 session 的隐藏 BrowserWindow 加载官方网页 mail.proton.me,由官方网页自己
|
|
6
|
+
// 完成 SRP 登录 + PGP 解密;我们只通过 webContents.executeJavaScript 读取「已解密的 DOM」
|
|
7
|
+
// 提取 6 位验证码。不劫持 webpack 内部模块、不碰 PGP,仅依赖少量 DOM 选择器。
|
|
8
|
+
//
|
|
9
|
+
// 登录态:首次需用户在弹出的窗口里手动登录一次(含 hCaptcha/2FA),之后通过
|
|
10
|
+
// partition='persist:proton' 持久化复用,无需重复登录。这正是 ElectronMail 的做法。
|
|
11
|
+
//
|
|
12
|
+
// 多地址「匿名」:配合 Proton catch-all 自定义域名 / SimpleLogin 别名,一个收件箱可收
|
|
13
|
+
// 无限个 prefix@yourdomain 的验证码邮件。
|
|
14
|
+
//
|
|
15
|
+
// WARN: 标注「Proton DOM 依赖点」的选择器随 Proton 改版可能失效,届时只需更新本文件。
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.openProtonLogin = openProtonLogin;
|
|
18
|
+
exports.getProtonLoginStatus = getProtonLoginStatus;
|
|
19
|
+
exports.closeProtonWindow = closeProtonWindow;
|
|
20
|
+
exports.waitProtonOtp = waitProtonOtp;
|
|
21
|
+
const electron_1 = require("electron");
|
|
22
|
+
const PARTITION = 'persist:proton';
|
|
23
|
+
// 真实 Chrome UA,去掉 Electron 标识,降低被 Proton 风控/拒绝的概率
|
|
24
|
+
const CHROME_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
|
25
|
+
// 验证码邮件一般落 inbox;如需覆盖垃圾箱可改为 all-mail
|
|
26
|
+
const PROTON_INBOX_URL = 'https://mail.proton.me/u/0/inbox';
|
|
27
|
+
let win = null;
|
|
28
|
+
/**
|
|
29
|
+
* 解析 Proton 窗口应使用的代理:优先显式传入,否则用「设置里的全局代理」
|
|
30
|
+
* (设置页经 set-proxy 写入 process.env.HTTPS_PROXY,见 main/index.ts applyProxySettings)。
|
|
31
|
+
* 注意:这里刻意用应用全局代理,而非注册代理池——代理池是逐账号轮换的临时出口,
|
|
32
|
+
* 不适合常驻、需要稳定登录态的取码窗口。
|
|
33
|
+
*/
|
|
34
|
+
function resolveSettingsProxy(explicit) {
|
|
35
|
+
const e = (explicit || '').trim();
|
|
36
|
+
if (e)
|
|
37
|
+
return e;
|
|
38
|
+
return (process.env.HTTPS_PROXY ||
|
|
39
|
+
process.env.https_proxy ||
|
|
40
|
+
process.env.HTTP_PROXY ||
|
|
41
|
+
process.env.http_proxy ||
|
|
42
|
+
'').trim();
|
|
43
|
+
}
|
|
44
|
+
function applyProxy(sess, proxy) {
|
|
45
|
+
const resolved = resolveSettingsProxy(proxy);
|
|
46
|
+
if (resolved) {
|
|
47
|
+
console.log(`[Proton] 走设置代理: ${resolved.replace(/:[^:@/]+@/, ':***@')}`);
|
|
48
|
+
return sess.setProxy({ proxyRules: resolved });
|
|
49
|
+
}
|
|
50
|
+
// 设置里未配代理 → 跟随系统代理(Proton 在受限网络下通常需要代理才能访问)
|
|
51
|
+
console.log('[Proton] 设置未配代理,跟随系统代理');
|
|
52
|
+
return sess.setProxy({ mode: 'system' });
|
|
53
|
+
}
|
|
54
|
+
/** 懒创建(或复用)Proton 窗口。show=true 时显示并聚焦,便于用户手动登录 */
|
|
55
|
+
async function ensureWindow(show, proxy) {
|
|
56
|
+
const sess = electron_1.session.fromPartition(PARTITION);
|
|
57
|
+
// 每次都按「设置里的代理」刷新 session 代理:设置变更后下次登录/取码即生效(窗口复用也更新)
|
|
58
|
+
await applyProxy(sess, proxy);
|
|
59
|
+
if (win && !win.isDestroyed()) {
|
|
60
|
+
if (show) {
|
|
61
|
+
win.show();
|
|
62
|
+
win.focus();
|
|
63
|
+
}
|
|
64
|
+
return win;
|
|
65
|
+
}
|
|
66
|
+
win = new electron_1.BrowserWindow({
|
|
67
|
+
width: 1024,
|
|
68
|
+
height: 800,
|
|
69
|
+
show,
|
|
70
|
+
title: 'Proton Mail',
|
|
71
|
+
autoHideMenuBar: true,
|
|
72
|
+
webPreferences: {
|
|
73
|
+
partition: PARTITION,
|
|
74
|
+
// 后台隐藏时不节流定时器/网络,保证 Proton 仍能实时收新邮件
|
|
75
|
+
backgroundThrottling: false,
|
|
76
|
+
contextIsolation: true,
|
|
77
|
+
nodeIntegration: false,
|
|
78
|
+
sandbox: true
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
win.webContents.setUserAgent(CHROME_UA);
|
|
82
|
+
// 站内导航(登录跳转等)保持在本窗口;其它外链交给系统浏览器
|
|
83
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
84
|
+
if (/proton\.me/i.test(url))
|
|
85
|
+
return { action: 'allow' };
|
|
86
|
+
return { action: 'deny' };
|
|
87
|
+
});
|
|
88
|
+
const closed = () => {
|
|
89
|
+
win = null;
|
|
90
|
+
};
|
|
91
|
+
win.on('closed', closed);
|
|
92
|
+
await loadAndWait(win, PROTON_INBOX_URL);
|
|
93
|
+
return win;
|
|
94
|
+
}
|
|
95
|
+
/** 加载 URL 并等待页面 dom-ready(带超时兜底) */
|
|
96
|
+
function loadAndWait(w, url, timeoutMs = 30000) {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
let done = false;
|
|
99
|
+
const finish = () => {
|
|
100
|
+
if (done)
|
|
101
|
+
return;
|
|
102
|
+
done = true;
|
|
103
|
+
w.webContents.removeListener('dom-ready', finish);
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
w.webContents.once('dom-ready', finish);
|
|
107
|
+
w.loadURL(url).catch(() => finish());
|
|
108
|
+
setTimeout(finish, timeoutMs);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function sleep(ms) {
|
|
112
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
113
|
+
}
|
|
114
|
+
/** 通过当前 URL + DOM 判断是否已登录 Proton Mail */
|
|
115
|
+
async function checkLoggedIn(w) {
|
|
116
|
+
const url = w.webContents.getURL();
|
|
117
|
+
// 未登录会跳到 account.proton.me 的 login/authorize 页
|
|
118
|
+
if (/account\.proton\.me/i.test(url) || /\/(login|authorize|switch)/i.test(url))
|
|
119
|
+
return false;
|
|
120
|
+
if (!/mail\.proton\.me\/u\//i.test(url))
|
|
121
|
+
return false;
|
|
122
|
+
// DOM 兜底确认:已登录时存在邮件列表容器,登录页存在密码输入框
|
|
123
|
+
try {
|
|
124
|
+
const ok = await w.webContents.executeJavaScript(`(() => {
|
|
125
|
+
if (document.querySelector('input[type="password"], #password')) return false
|
|
126
|
+
const sels = ['[data-testid="message-list"]','.items-column-list','[data-shortcut-target="item-container"]','main [role="main"]']
|
|
127
|
+
return sels.some(s => document.querySelector(s)) || /\\/u\\//.test(location.pathname)
|
|
128
|
+
})()`, false);
|
|
129
|
+
return Boolean(ok);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return /mail\.proton\.me\/u\//i.test(url);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** 打开 Proton 登录窗口(显示),返回当前登录态。用户在窗口内手动完成登录 */
|
|
136
|
+
async function openProtonLogin(proxy) {
|
|
137
|
+
try {
|
|
138
|
+
const w = await ensureWindow(true, proxy);
|
|
139
|
+
// 给页面一点时间完成 Proton 的自动跳转(已登录直达 inbox / 未登录跳 login)
|
|
140
|
+
await sleep(1200);
|
|
141
|
+
const loggedIn = await checkLoggedIn(w);
|
|
142
|
+
return { success: true, loggedIn };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return { success: false, loggedIn: false, error: err instanceof Error ? err.message : String(err) };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** 查询当前 Proton 登录态(不弹窗:窗口不存在则后台静默创建检测) */
|
|
149
|
+
async function getProtonLoginStatus(proxy) {
|
|
150
|
+
try {
|
|
151
|
+
const w = await ensureWindow(false, proxy);
|
|
152
|
+
await sleep(600);
|
|
153
|
+
return { loggedIn: await checkLoggedIn(w) };
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return { loggedIn: false };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** 关闭 Proton 窗口(保留持久化 session,下次免登录) */
|
|
160
|
+
function closeProtonWindow() {
|
|
161
|
+
if (win && !win.isDestroyed())
|
|
162
|
+
win.destroy();
|
|
163
|
+
win = null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 在已解密的收件箱中查找「发给当前注册地址」那封 AWS 验证码邮件的验证码。
|
|
167
|
+
* 三重定位(鲁棒优先):
|
|
168
|
+
* 1) 发件人筛选:只看发件人为 no-reply@signin.aws 的邮件,排除「Response Required」等非验证码 AWS 邮件;
|
|
169
|
+
* 2) 读前两封:验证码邮件有时不是最新一封(会被其它邮件挤后),故对候选最多点开两封;
|
|
170
|
+
* 3) 收件人精确匹配:用邮件头收件人原样带点地址区分同母号不同点号变体的旧码。
|
|
171
|
+
* 收件人读不到时,因发件人已确认是 AWS 验证码邮件,退化为信任有码的那封。
|
|
172
|
+
* @param address 当前注册使用的收件地址(点号变体,原样含点)
|
|
173
|
+
*/
|
|
174
|
+
function buildScanScript(address) {
|
|
175
|
+
const addrFull = JSON.stringify(address.trim().toLowerCase());
|
|
176
|
+
return `(async () => {
|
|
177
|
+
const addrFull = ${addrFull};
|
|
178
|
+
const extractCode = (t) => { const m = (t||'').match(/\\b\\d{6}\\b/g); return m ? m[m.length-1] : ''; };
|
|
179
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
180
|
+
const fire = (el, type) => el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
|
|
181
|
+
// 读取当前打开邮件的收件人地址集合(Proton DOM 依赖点:mailto / recipient-label / recipients:item-)
|
|
182
|
+
const readRecipients = () => {
|
|
183
|
+
const set = new Set();
|
|
184
|
+
document.querySelectorAll('a[href^="mailto:"]').forEach((a) => {
|
|
185
|
+
const m = (a.getAttribute('href') || '').replace(/^mailto:/i, '').trim().toLowerCase();
|
|
186
|
+
if (m.indexOf('@') > 0) set.add(m);
|
|
187
|
+
});
|
|
188
|
+
document.querySelectorAll('[data-testid="recipient-label"], bdi.message-recipient-item-label').forEach((el) => {
|
|
189
|
+
const t = (el.innerText || '').trim().toLowerCase();
|
|
190
|
+
if (t.indexOf('@') > 0) set.add(t);
|
|
191
|
+
});
|
|
192
|
+
document.querySelectorAll('[data-testid^="recipients:item-"]').forEach((el) => {
|
|
193
|
+
const t = (el.getAttribute('data-testid') || '').replace('recipients:item-', '').trim().toLowerCase();
|
|
194
|
+
if (t.indexOf('@') > 0) set.add(t);
|
|
195
|
+
});
|
|
196
|
+
return set;
|
|
197
|
+
};
|
|
198
|
+
// 列表项发件人地址(Proton DOM 依赖点):AWS 验证码邮件发件人固定为 no-reply@signin.aws,
|
|
199
|
+
// 用它精确筛掉同为 AWS 的非验证码邮件(如「Response Required: Your Kiro Account」,
|
|
200
|
+
// 那封收件人也是当前地址,仅靠收件人校验会误判「匹配但无码」而卡住)。
|
|
201
|
+
const SENDER = 'no-reply@signin.aws';
|
|
202
|
+
const senderOf = (it) => {
|
|
203
|
+
const el = it.querySelector('[data-testid="message-column:sender-address"]');
|
|
204
|
+
return el ? (el.getAttribute('title') || el.innerText || '').trim().toLowerCase() : '';
|
|
205
|
+
};
|
|
206
|
+
// 打开某封邮件:避开行内星标 button / 复选框 input,否则只会切换星标而打不开邮件
|
|
207
|
+
const openItem = (it) => {
|
|
208
|
+
let target = it.querySelector('[data-testid="message-column:subject"]')
|
|
209
|
+
|| it.querySelector('[data-testid^="message-row"]')
|
|
210
|
+
|| it.querySelector('.item-subject-wrapper, .subject, span[role="heading"]');
|
|
211
|
+
if (!target) {
|
|
212
|
+
const cand = Array.from(it.querySelectorAll('span, div'))
|
|
213
|
+
.filter((el) => !el.closest('button') && !el.querySelector('button, input') && (el.innerText || '').trim().length > 8);
|
|
214
|
+
target = cand[0] || it;
|
|
215
|
+
}
|
|
216
|
+
fire(target, 'mousedown'); fire(target, 'mouseup'); fire(target, 'click');
|
|
217
|
+
};
|
|
218
|
+
// 读正文(Proton 正文渲染在 iframe 内,优先读 iframe)
|
|
219
|
+
const readBody = () => {
|
|
220
|
+
let body = '';
|
|
221
|
+
const ifr = document.querySelector('iframe[data-testid="content-iframe"], iframe[title], iframe');
|
|
222
|
+
if (ifr) { try { body = (ifr.contentDocument && ifr.contentDocument.body) ? (ifr.contentDocument.body.innerText || '') : ''; } catch (e) {} }
|
|
223
|
+
if (!body) {
|
|
224
|
+
const readSels = ['[data-testid="message-content"]','.message-content','[data-testid="message-view"]','main [role="article"]','main'];
|
|
225
|
+
for (const rs of readSels) { const el = document.querySelector(rs); if (el && el.innerText) { body = el.innerText; break; } }
|
|
226
|
+
}
|
|
227
|
+
if (!body) body = document.body.innerText || '';
|
|
228
|
+
return body;
|
|
229
|
+
};
|
|
230
|
+
// Proton DOM 依赖点:邮件列表项候选选择器(多重兜底)
|
|
231
|
+
const listSels = ['[data-testid="message-item"]','[data-shortcut-target="item-container"]','.items-column-list [role="row"]','.item-container-wrapper','.item-container'];
|
|
232
|
+
let items = [];
|
|
233
|
+
for (const s of listSels) { const e = [...document.querySelectorAll(s)]; if (e.length) { items = e; break; } }
|
|
234
|
+
if (!items[0]) return { code: '', from: 'none', matched: false };
|
|
235
|
+
// 优先只看发件人为 AWS 验证码地址的邮件;筛不到时回退看前两封(兜底,防发件人 DOM 改版)
|
|
236
|
+
const awsItems = items.filter((it) => senderOf(it) === SENDER);
|
|
237
|
+
const candidates = (awsItems.length ? awsItems : items).slice(0, 2);
|
|
238
|
+
const results = [];
|
|
239
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
240
|
+
try {
|
|
241
|
+
openItem(candidates[i]);
|
|
242
|
+
// 轮询等渲染就绪(出现 6 位码 / 收件人+正文齐备)即提前继续,省去固定死等 2.2s。
|
|
243
|
+
// 首次稍等 iframe 切到新邮件,之后细粒度轮询;上限 ~2s 与原死等相当但通常 0.5s 内命中。
|
|
244
|
+
let body = '';
|
|
245
|
+
let recipients = new Set();
|
|
246
|
+
for (let t = 0; t < 11; t++) {
|
|
247
|
+
await sleep(t === 0 ? 350 : 170);
|
|
248
|
+
body = readBody();
|
|
249
|
+
recipients = readRecipients();
|
|
250
|
+
if (extractCode(body) || (recipients.size > 0 && body.length > 30)) break;
|
|
251
|
+
}
|
|
252
|
+
const r = {
|
|
253
|
+
i,
|
|
254
|
+
hasRecip: recipients.size > 0,
|
|
255
|
+
match: recipients.has(addrFull),
|
|
256
|
+
code: extractCode(body),
|
|
257
|
+
recipText: Array.from(recipients).join(',').slice(0, 100),
|
|
258
|
+
bodySnip: body.slice(0, 100)
|
|
259
|
+
};
|
|
260
|
+
results.push(r);
|
|
261
|
+
// 早停:收件人精确匹配 + 有码 → 当前注册地址那封的验证码(最高置信)
|
|
262
|
+
if (r.match && r.code) return { code: r.code, from: 'body', matched: true, snippet: 'aws#' + i + ' ' + r.bodySnip };
|
|
263
|
+
} catch (e) {
|
|
264
|
+
results.push({ i, hasRecip: false, match: false, code: '', recipText: '', bodySnip: 'err=' + String(e) });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// 收件人读不到但有码(发件人已确认是 AWS 验证码邮件,可信)→ 退化采用
|
|
268
|
+
const noRecipCode = results.find((r) => !r.hasRecip && r.code);
|
|
269
|
+
if (noRecipCode) return { code: noRecipCode.code, from: 'body', matched: false, snippet: 'aws#' + noRecipCode.i + ' no-recipients; ' + noRecipCode.bodySnip };
|
|
270
|
+
// 收件人精确匹配但还没读到码(邮件刚到正在渲染)
|
|
271
|
+
const matchNoCode = results.find((r) => r.match && !r.code);
|
|
272
|
+
if (matchNoCode) return { code: '', from: 'body-nocode', matched: true, snippet: 'aws#' + matchNoCode.i + ' ' + matchNoCode.bodySnip };
|
|
273
|
+
// 有码但收件人是别的变体 → 不是当前的,继续等
|
|
274
|
+
const wrongRecip = results.find((r) => r.code && r.hasRecip && !r.match);
|
|
275
|
+
if (wrongRecip) return { code: '', from: 'wrong-recipient', matched: false, snippet: 'aws#' + wrongRecip.i + ' recipients=' + wrongRecip.recipText };
|
|
276
|
+
return { code: '', from: 'body-nocode', matched: false, snippet: 'awsItems=' + awsItems.length + '; ' + results.map((r) => '#' + r.i + (r.code ? '+code' : '-nocode') + ' r=' + (r.recipText || 'none')).join(' | ').slice(0, 170) };
|
|
277
|
+
})()`;
|
|
278
|
+
}
|
|
279
|
+
// 单窗口单收件箱:并发取码会互相打架(同时导航/点开同一窗口),故串行化排队。
|
|
280
|
+
// 批量并发注册时建议把 Proton 源的并发设为 1;即便设了更高并发,这里也会强制串行取码。
|
|
281
|
+
let otpQueue = Promise.resolve();
|
|
282
|
+
function waitProtonOtp(address, opts) {
|
|
283
|
+
const run = otpQueue.then(() => runWaitProtonOtp(address, opts), () => runWaitProtonOtp(address, opts));
|
|
284
|
+
// 保持队列链不被某次 reject 中断
|
|
285
|
+
otpQueue = run.catch(() => undefined);
|
|
286
|
+
return run;
|
|
287
|
+
}
|
|
288
|
+
async function runWaitProtonOtp(address, opts) {
|
|
289
|
+
const log = opts.log ?? (() => { });
|
|
290
|
+
const w = await ensureWindow(false, opts.proxy);
|
|
291
|
+
if (!(await checkLoggedIn(w))) {
|
|
292
|
+
throw new Error('Proton 未登录,请先在「登录 Proton」窗口完成登录');
|
|
293
|
+
}
|
|
294
|
+
// 取码前导航到 inbox 确保处于最新收件箱视图
|
|
295
|
+
await loadAndWait(w, PROTON_INBOX_URL);
|
|
296
|
+
await sleep(1500);
|
|
297
|
+
// Proton 取码是「点开邮件读本地 DOM」,无网络 API 限流顾虑,且点开本身已自带耗时,
|
|
298
|
+
// 故轮询间隔不沿用网络型邮箱的 intervalSec(默认3s),钳到 ≤1s 让验证码到达后更快被读到。
|
|
299
|
+
const pollMs = Math.min(Math.max(opts.intervalSec * 1000, 250), 1000);
|
|
300
|
+
const maxRetries = Math.max(1, Math.floor((opts.timeoutSec * 1000) / pollMs));
|
|
301
|
+
const script = buildScanScript(address);
|
|
302
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
303
|
+
if (opts.signal?.aborted)
|
|
304
|
+
throw new Error('注册已取消');
|
|
305
|
+
// 每 ~20 轮 reload 一次兜底(防止 SPA 长时间不刷新漏收)
|
|
306
|
+
if (attempt > 1 && attempt % 20 === 0) {
|
|
307
|
+
await loadAndWait(w, PROTON_INBOX_URL);
|
|
308
|
+
await sleep(1200);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const res = (await w.webContents.executeJavaScript(script, true));
|
|
312
|
+
if (res && res.code && res.from === 'body') {
|
|
313
|
+
// 收件人精确匹配当前注册地址(或收件人读不到时正文去点匹配)+ 读到 6 位码 → 当前邮件的验证码
|
|
314
|
+
log(`[Proton] 验证码: ${res.code} (${res.matched ? '收件人精确匹配' : '正文去点兜底匹配'})`);
|
|
315
|
+
return res.code;
|
|
316
|
+
}
|
|
317
|
+
else if (res && res.from === 'wrong-recipient') {
|
|
318
|
+
if (attempt % 8 === 0)
|
|
319
|
+
log(`[Proton] 最新邮件收件人非当前地址,等待当前验证码... ${res.snippet || ''}`);
|
|
320
|
+
}
|
|
321
|
+
else if (res && res.from === 'body-nocode') {
|
|
322
|
+
if (attempt % 8 === 0)
|
|
323
|
+
log(`[Proton] ${res.matched ? '已打开当前邮件但未提取到码' : '暂无匹配邮件'}: ${res.snippet || ''}`);
|
|
324
|
+
}
|
|
325
|
+
else if (res && res.from === 'error') {
|
|
326
|
+
if (attempt % 10 === 0)
|
|
327
|
+
log(`[Proton] 取码脚本异常: ${res.err}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
if (attempt % 10 === 0)
|
|
332
|
+
log(`[Proton] [${attempt}/${maxRetries}] 读取失败: ${err}`);
|
|
333
|
+
}
|
|
334
|
+
if (attempt % 10 === 0)
|
|
335
|
+
log(`[Proton] [${attempt}/${maxRetries}] 暂无验证码...`);
|
|
336
|
+
await sleep(pollMs);
|
|
337
|
+
}
|
|
338
|
+
throw new Error(`等待验证码超时 (${opts.timeoutSec}s)`);
|
|
339
|
+
}
|