Skip to content

Commit da2ad06

Browse files
authored
Merge pull request #133 from aicrossai/main
Feature: Distributed Captcha Extension, VEO Video Extend & Timeout Optimization
2 parents 4b7a0ad + abd0c00 commit da2ad06

20 files changed

Lines changed: 1898 additions & 229 deletions

config/setting.toml

Lines changed: 0 additions & 76 deletions
This file was deleted.

config/setting_example.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ timeout = 7200 # 缓存超时时间(秒), 默认2小时; 设置为0表示不自
5454
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
5555

5656
[captcha]
57-
captcha_method = "browser" # 打码方式: yescaptcha/browser/personal/remote_browser
58-
browser_launch_background = true # 有头浏览器是否默认后台启动;设为 false 可直接看到窗口
57+
captcha_method = "extension" # 打码方式: extension/yescaptcha/browser/personal/remote_browser
5958
browser_recaptcha_settle_seconds = 3.0 # reload/clr 就绪后的额外稳态等待
6059
browser_count = 1 # browser 模式的有头浏览器实例数量
6160
personal_project_pool_size = 4 # personal 模式下单个 Token 默认维护的项目池数量(仅影响项目轮换,不决定打码标签页数量)
@@ -66,3 +65,5 @@ yescaptcha_base_url = "https://api.yescaptcha.com"
6665
remote_browser_base_url = "" # 远程有头打码服务地址
6766
remote_browser_api_key = "" # 远程有头打码服务 API Key
6867
remote_browser_timeout = 60 # 远程有头打码请求超时(秒)
68+
capsolver_api_key = ""
69+
capsolver_base_url = "https://api.capsolver.com"

extension/background.js

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
let ws = null;
2+
let reconnectTimeout = null;
3+
let heartbeatInterval = null;
4+
5+
const DEFAULT_SETTINGS = {
6+
serverUrl: "ws://127.0.0.1:8000/captcha_ws",
7+
apiKey: "",
8+
routeKey: "",
9+
clientLabel: ""
10+
};
11+
12+
function getSettings() {
13+
return new Promise((resolve) => {
14+
chrome.storage.local.get(DEFAULT_SETTINGS, (stored) => {
15+
resolve({
16+
serverUrl: (stored.serverUrl || DEFAULT_SETTINGS.serverUrl).trim(),
17+
apiKey: (stored.apiKey || "").trim(),
18+
routeKey: (stored.routeKey || "").trim(),
19+
clientLabel: (stored.clientLabel || "").trim()
20+
});
21+
});
22+
});
23+
}
24+
25+
function closeSocket() {
26+
if (heartbeatInterval) clearInterval(heartbeatInterval);
27+
heartbeatInterval = null;
28+
if (reconnectTimeout) clearTimeout(reconnectTimeout);
29+
reconnectTimeout = null;
30+
if (ws) {
31+
try {
32+
ws.close();
33+
} catch (e) {
34+
console.log("[Flow2API] Close socket error", e);
35+
}
36+
ws = null;
37+
}
38+
}
39+
40+
function sleep(ms) {
41+
return new Promise(resolve => setTimeout(resolve, ms));
42+
}
43+
44+
function waitForTabReady(tabId, timeoutMs = 12000) {
45+
return new Promise((resolve) => {
46+
let settled = false;
47+
const finish = () => {
48+
if (settled) return;
49+
settled = true;
50+
chrome.tabs.onUpdated.removeListener(onUpdated);
51+
clearTimeout(timer);
52+
resolve();
53+
};
54+
const onUpdated = (updatedTabId, changeInfo) => {
55+
if (updatedTabId === tabId && changeInfo.status === "complete") {
56+
finish();
57+
}
58+
};
59+
const timer = setTimeout(finish, timeoutMs);
60+
61+
chrome.tabs.onUpdated.addListener(onUpdated);
62+
chrome.tabs.get(tabId, (tab) => {
63+
if (chrome.runtime.lastError) {
64+
finish();
65+
return;
66+
}
67+
if (tab && tab.status === "complete") {
68+
finish();
69+
}
70+
});
71+
});
72+
}
73+
74+
async function connectWS() {
75+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
76+
77+
const settings = await getSettings();
78+
const url = new URL(settings.serverUrl || DEFAULT_SETTINGS.serverUrl);
79+
if (settings.apiKey) {
80+
url.searchParams.set("key", settings.apiKey);
81+
}
82+
if (settings.routeKey) {
83+
url.searchParams.set("route_key", settings.routeKey);
84+
}
85+
if (settings.clientLabel) {
86+
url.searchParams.set("client_label", settings.clientLabel);
87+
}
88+
89+
ws = new WebSocket(url.toString());
90+
91+
ws.onopen = () => {
92+
console.log("[Flow2API] Background connected to WebSocket", url.toString());
93+
ws.send(JSON.stringify({
94+
type: "register",
95+
route_key: settings.routeKey,
96+
client_label: settings.clientLabel
97+
}));
98+
if (heartbeatInterval) clearInterval(heartbeatInterval);
99+
heartbeatInterval = setInterval(() => {
100+
if (ws && ws.readyState === WebSocket.OPEN) {
101+
ws.send(JSON.stringify({ type: "ping" }));
102+
}
103+
}, 20000);
104+
};
105+
106+
let tokenQueue = Promise.resolve();
107+
108+
ws.onmessage = async (event) => {
109+
let data;
110+
try {
111+
data = JSON.parse(event.data);
112+
} catch (e) {
113+
return;
114+
}
115+
116+
if (data.type === "register_ack") {
117+
console.log("[Flow2API] Registered route key:", data.route_key || "(empty)");
118+
return;
119+
}
120+
121+
if (data.type === "get_token") {
122+
tokenQueue = tokenQueue.then(() => handleGetToken(data)).catch(err => {
123+
console.error("[Flow2API] Queue Error:", err);
124+
});
125+
}
126+
};
127+
128+
ws.onclose = () => {
129+
console.log("[Flow2API] WebSocket Closed. Reconnecting in 2s...");
130+
ws = null;
131+
if (heartbeatInterval) clearInterval(heartbeatInterval);
132+
if (reconnectTimeout) clearTimeout(reconnectTimeout);
133+
reconnectTimeout = setTimeout(connectWS, 2000);
134+
};
135+
136+
ws.onerror = (e) => {
137+
console.log("[Flow2API] WebSocket Error", e);
138+
};
139+
}
140+
141+
async function handleGetToken(data) {
142+
let newTabId = null;
143+
try {
144+
console.log("[Flow2API] Auto-opening fresh Google Labs tab to avoid token expiry...");
145+
const newTab = await chrome.tabs.create({ url: "https://labs.google/fx/tools/flow", active: false });
146+
newTabId = newTab.id;
147+
148+
await waitForTabReady(newTabId);
149+
await sleep(1200);
150+
151+
let successResponse = null;
152+
let lastErrorMsg = "No response from tab.";
153+
const scriptTimeoutMs = data.action === "VIDEO_GENERATION" ? 30000 : 20000;
154+
155+
try {
156+
const results = await chrome.scripting.executeScript({
157+
target: { tabId: newTabId },
158+
world: "MAIN",
159+
func: async (action, timeoutMs) => {
160+
return new Promise((resolve, reject) => {
161+
let settled = false;
162+
const finish = (fn, value) => {
163+
if (settled) return;
164+
settled = true;
165+
fn(value);
166+
};
167+
try {
168+
function run() {
169+
grecaptcha.enterprise.ready(function() {
170+
grecaptcha.enterprise.execute("6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV", { action: action })
171+
.then(token => finish(resolve, token))
172+
.catch(err => finish(reject, err.message || "reCAPTCHA evaluation failed internally"));
173+
});
174+
}
175+
176+
if (typeof grecaptcha !== "undefined" && grecaptcha.enterprise) {
177+
run();
178+
} else {
179+
const s = document.createElement("script");
180+
s.src = "https://www.google.com/recaptcha/enterprise.js?render=6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV";
181+
s.onload = run;
182+
s.onerror = () => finish(reject, "Failed to load enterprise.js via network");
183+
document.head.appendChild(s);
184+
}
185+
186+
setTimeout(() => finish(reject, "Timeout generating reCAPTCHA locally"), timeoutMs);
187+
} catch (e) {
188+
finish(reject, e.message);
189+
}
190+
});
191+
},
192+
args: [data.action || "IMAGE_GENERATION", scriptTimeoutMs]
193+
});
194+
195+
if (results && results[0] && results[0].result) {
196+
successResponse = { status: "success", token: results[0].result };
197+
}
198+
} catch (e) {
199+
lastErrorMsg = e.message || "Script execution failed";
200+
}
201+
202+
if (successResponse) {
203+
ws.send(JSON.stringify({
204+
req_id: data.req_id,
205+
status: successResponse.status,
206+
token: successResponse.token
207+
}));
208+
} else {
209+
ws.send(JSON.stringify({
210+
req_id: data.req_id,
211+
status: "error",
212+
error: "Extension script failed: " + lastErrorMsg
213+
}));
214+
}
215+
} catch (err) {
216+
ws.send(JSON.stringify({
217+
req_id: data.req_id,
218+
status: "error",
219+
error: err.message
220+
}));
221+
} finally {
222+
if (newTabId) {
223+
try {
224+
await chrome.tabs.remove(newTabId);
225+
console.log("[Flow2API] Closed temporary token tab.");
226+
} catch (e) {
227+
console.log("[Flow2API] Error closing tab:", e);
228+
}
229+
}
230+
}
231+
}
232+
233+
chrome.storage.onChanged.addListener((changes, areaName) => {
234+
if (areaName !== "local") return;
235+
if (changes.routeKey || changes.serverUrl || changes.apiKey || changes.clientLabel) {
236+
console.log("[Flow2API] Extension settings changed, reconnecting WebSocket...");
237+
closeSocket();
238+
connectWS();
239+
}
240+
});
241+
242+
connectWS();

extension/content.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
console.log("[Flow2API] Captcha Worker injected.");
2+
3+
function getRecaptchaToken(action) {
4+
return new Promise((resolve, reject) => {
5+
const reqId = Date.now() + Math.random().toString();
6+
const script = document.createElement("script");
7+
script.textContent = `
8+
try {
9+
function runCaptcha() {
10+
grecaptcha.enterprise.ready(function() {
11+
grecaptcha.enterprise.execute('6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV', {action: '${action}'})
12+
.then(token => window.postMessage({type: 'reCAPTCHA_result', reqId: '${reqId}', token: token}, '*'))
13+
.catch(err => window.postMessage({type: 'reCAPTCHA_error', reqId: '${reqId}', error: err.message}, '*'));
14+
});
15+
}
16+
17+
if (typeof grecaptcha !== "undefined" && grecaptcha.enterprise) {
18+
runCaptcha();
19+
} else {
20+
const rScript = document.createElement('script');
21+
rScript.src = "https://www.google.com/recaptcha/enterprise.js?render=6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV";
22+
rScript.onload = () => { runCaptcha(); };
23+
rScript.onerror = () => { window.postMessage({type: 'reCAPTCHA_error', reqId: '${reqId}', error: 'Failed to load enterprise.js'}, '*'); };
24+
document.head.appendChild(rScript);
25+
}
26+
} catch (e) {
27+
window.postMessage({type: 'reCAPTCHA_error', reqId: '${reqId}', error: e.message}, '*');
28+
}
29+
`;
30+
31+
const listener = (event) => {
32+
if (event.source !== window || !event.data) return;
33+
if (event.data.reqId === reqId) {
34+
window.removeEventListener("message", listener);
35+
script.remove();
36+
if (event.data.type === 'reCAPTCHA_result') {
37+
resolve(event.data.token);
38+
} else {
39+
reject(new Error(event.data.error || "Unknown reCAPTCHA Error"));
40+
}
41+
}
42+
};
43+
window.addEventListener("message", listener);
44+
document.documentElement.appendChild(script);
45+
46+
setTimeout(() => {
47+
window.removeEventListener("message", listener);
48+
script.remove();
49+
reject(new Error("Timeout generating reCAPTCHA"));
50+
}, 15000);
51+
});
52+
}
53+
54+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
55+
if (message.type === "get_token") {
56+
console.log("[Flow2API] Generating token for action: " + message.action);
57+
getRecaptchaToken(message.action)
58+
.then(token => sendResponse({status: "success", token: token}))
59+
.catch(err => sendResponse({status: "error", error: err.message}));
60+
return true;
61+
}
62+
});
63+

0 commit comments

Comments
 (0)