Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 0 additions & 76 deletions config/setting.toml

This file was deleted.

5 changes: 3 additions & 2 deletions config/setting_example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ timeout = 7200 # 缓存超时时间(秒), 默认2小时; 设置为0表示不自
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址

[captcha]
captcha_method = "browser" # 打码方式: yescaptcha/browser/personal/remote_browser
browser_launch_background = true # 有头浏览器是否默认后台启动;设为 false 可直接看到窗口
captcha_method = "extension" # 打码方式: extension/yescaptcha/browser/personal/remote_browser
browser_recaptcha_settle_seconds = 3.0 # reload/clr 就绪后的额外稳态等待
browser_count = 1 # browser 模式的有头浏览器实例数量
personal_project_pool_size = 4 # personal 模式下单个 Token 默认维护的项目池数量(仅影响项目轮换,不决定打码标签页数量)
Expand All @@ -66,3 +65,5 @@ yescaptcha_base_url = "https://api.yescaptcha.com"
remote_browser_base_url = "" # 远程有头打码服务地址
remote_browser_api_key = "" # 远程有头打码服务 API Key
remote_browser_timeout = 60 # 远程有头打码请求超时(秒)
capsolver_api_key = ""
capsolver_base_url = "https://api.capsolver.com"
242 changes: 242 additions & 0 deletions extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
let ws = null;
let reconnectTimeout = null;
let heartbeatInterval = null;

const DEFAULT_SETTINGS = {
serverUrl: "ws://127.0.0.1:8000/captcha_ws",
apiKey: "",
routeKey: "",
clientLabel: ""
};

function getSettings() {
return new Promise((resolve) => {
chrome.storage.local.get(DEFAULT_SETTINGS, (stored) => {
resolve({
serverUrl: (stored.serverUrl || DEFAULT_SETTINGS.serverUrl).trim(),
apiKey: (stored.apiKey || "").trim(),
routeKey: (stored.routeKey || "").trim(),
clientLabel: (stored.clientLabel || "").trim()
});
});
});
}

function closeSocket() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = null;
if (reconnectTimeout) clearTimeout(reconnectTimeout);
reconnectTimeout = null;
if (ws) {
try {
ws.close();
} catch (e) {
console.log("[Flow2API] Close socket error", e);
}
ws = null;
}
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

function waitForTabReady(tabId, timeoutMs = 12000) {
return new Promise((resolve) => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
chrome.tabs.onUpdated.removeListener(onUpdated);
clearTimeout(timer);
resolve();
};
const onUpdated = (updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === "complete") {
finish();
}
};
const timer = setTimeout(finish, timeoutMs);

chrome.tabs.onUpdated.addListener(onUpdated);
chrome.tabs.get(tabId, (tab) => {
if (chrome.runtime.lastError) {
finish();
return;
}
if (tab && tab.status === "complete") {
finish();
}
});
});
}

async function connectWS() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;

const settings = await getSettings();
const url = new URL(settings.serverUrl || DEFAULT_SETTINGS.serverUrl);
if (settings.apiKey) {
url.searchParams.set("key", settings.apiKey);
}
if (settings.routeKey) {
url.searchParams.set("route_key", settings.routeKey);
}
if (settings.clientLabel) {
url.searchParams.set("client_label", settings.clientLabel);
}

ws = new WebSocket(url.toString());

ws.onopen = () => {
console.log("[Flow2API] Background connected to WebSocket", url.toString());
ws.send(JSON.stringify({
type: "register",
route_key: settings.routeKey,
client_label: settings.clientLabel
}));
if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }));
}
}, 20000);
};

let tokenQueue = Promise.resolve();

ws.onmessage = async (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
return;
}

if (data.type === "register_ack") {
console.log("[Flow2API] Registered route key:", data.route_key || "(empty)");
return;
}

if (data.type === "get_token") {
tokenQueue = tokenQueue.then(() => handleGetToken(data)).catch(err => {
console.error("[Flow2API] Queue Error:", err);
});
}
};

ws.onclose = () => {
console.log("[Flow2API] WebSocket Closed. Reconnecting in 2s...");
ws = null;
if (heartbeatInterval) clearInterval(heartbeatInterval);
if (reconnectTimeout) clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(connectWS, 2000);
};

ws.onerror = (e) => {
console.log("[Flow2API] WebSocket Error", e);
};
}

async function handleGetToken(data) {
let newTabId = null;
try {
console.log("[Flow2API] Auto-opening fresh Google Labs tab to avoid token expiry...");
const newTab = await chrome.tabs.create({ url: "https://labs.google/fx/tools/flow", active: false });
newTabId = newTab.id;

await waitForTabReady(newTabId);
await sleep(1200);

let successResponse = null;
let lastErrorMsg = "No response from tab.";
const scriptTimeoutMs = data.action === "VIDEO_GENERATION" ? 30000 : 20000;

try {
const results = await chrome.scripting.executeScript({
target: { tabId: newTabId },
world: "MAIN",
func: async (action, timeoutMs) => {
return new Promise((resolve, reject) => {
let settled = false;
const finish = (fn, value) => {
if (settled) return;
settled = true;
fn(value);
};
try {
function run() {
grecaptcha.enterprise.ready(function() {
grecaptcha.enterprise.execute("6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV", { action: action })
.then(token => finish(resolve, token))
.catch(err => finish(reject, err.message || "reCAPTCHA evaluation failed internally"));
});
}

if (typeof grecaptcha !== "undefined" && grecaptcha.enterprise) {
run();
} else {
const s = document.createElement("script");
s.src = "https://www.google.com/recaptcha/enterprise.js?render=6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV";
s.onload = run;
s.onerror = () => finish(reject, "Failed to load enterprise.js via network");
document.head.appendChild(s);
}

setTimeout(() => finish(reject, "Timeout generating reCAPTCHA locally"), timeoutMs);
} catch (e) {
finish(reject, e.message);
}
});
},
args: [data.action || "IMAGE_GENERATION", scriptTimeoutMs]
});

if (results && results[0] && results[0].result) {
successResponse = { status: "success", token: results[0].result };
}
} catch (e) {
lastErrorMsg = e.message || "Script execution failed";
}

if (successResponse) {
ws.send(JSON.stringify({
req_id: data.req_id,
status: successResponse.status,
token: successResponse.token
}));
} else {
ws.send(JSON.stringify({
req_id: data.req_id,
status: "error",
error: "Extension script failed: " + lastErrorMsg
}));
}
} catch (err) {
ws.send(JSON.stringify({
req_id: data.req_id,
status: "error",
error: err.message
}));
} finally {
if (newTabId) {
try {
await chrome.tabs.remove(newTabId);
console.log("[Flow2API] Closed temporary token tab.");
} catch (e) {
console.log("[Flow2API] Error closing tab:", e);
}
}
}
}

chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "local") return;
if (changes.routeKey || changes.serverUrl || changes.apiKey || changes.clientLabel) {
console.log("[Flow2API] Extension settings changed, reconnecting WebSocket...");
closeSocket();
connectWS();
}
});

connectWS();
63 changes: 63 additions & 0 deletions extension/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
console.log("[Flow2API] Captcha Worker injected.");

function getRecaptchaToken(action) {
return new Promise((resolve, reject) => {
const reqId = Date.now() + Math.random().toString();
const script = document.createElement("script");
script.textContent = `
try {
function runCaptcha() {
grecaptcha.enterprise.ready(function() {
grecaptcha.enterprise.execute('6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV', {action: '${action}'})
.then(token => window.postMessage({type: 'reCAPTCHA_result', reqId: '${reqId}', token: token}, '*'))
.catch(err => window.postMessage({type: 'reCAPTCHA_error', reqId: '${reqId}', error: err.message}, '*'));
});
}

if (typeof grecaptcha !== "undefined" && grecaptcha.enterprise) {
runCaptcha();
} else {
const rScript = document.createElement('script');
rScript.src = "https://www.google.com/recaptcha/enterprise.js?render=6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV";
rScript.onload = () => { runCaptcha(); };
rScript.onerror = () => { window.postMessage({type: 'reCAPTCHA_error', reqId: '${reqId}', error: 'Failed to load enterprise.js'}, '*'); };
document.head.appendChild(rScript);
}
} catch (e) {
window.postMessage({type: 'reCAPTCHA_error', reqId: '${reqId}', error: e.message}, '*');
}
`;

const listener = (event) => {
if (event.source !== window || !event.data) return;
if (event.data.reqId === reqId) {
window.removeEventListener("message", listener);
script.remove();
if (event.data.type === 'reCAPTCHA_result') {
resolve(event.data.token);
} else {
reject(new Error(event.data.error || "Unknown reCAPTCHA Error"));
}
}
};
window.addEventListener("message", listener);
document.documentElement.appendChild(script);

setTimeout(() => {
window.removeEventListener("message", listener);
script.remove();
reject(new Error("Timeout generating reCAPTCHA"));
}, 15000);
});
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "get_token") {
console.log("[Flow2API] Generating token for action: " + message.action);
getRecaptchaToken(message.action)
.then(token => sendResponse({status: "success", token: token}))
.catch(err => sendResponse({status: "error", error: err.message}));
return true;
}
});

Loading