mirror of
https://github.com/TheSmallHanCat/flow2api.git
synced 2026-06-01 04:11:55 +08:00
Fix PR 133 merge integration issues
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 866 KiB |
@@ -4,14 +4,12 @@ services:
|
||||
flow2api:
|
||||
image: ghcr.io/thesmallhancat/flow2api:latest
|
||||
container_name: flow2api
|
||||
network_mode: host
|
||||
ports:
|
||||
- "38000:8000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./tmp:/app/tmp
|
||||
- ./config/setting.toml:/app/config/setting.toml
|
||||
- ./src/services/proxy_manager.py:/app/src/services/proxy_manager.py
|
||||
- ./src/services/flow_client.py:/app/src/services/flow_client.py
|
||||
- ./src/api/admin.py:/app/src/api/admin.py
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -4,6 +4,7 @@ let heartbeatInterval = null;
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
serverUrl: "ws://127.0.0.1:8000/captcha_ws",
|
||||
apiKey: "",
|
||||
routeKey: "",
|
||||
clientLabel: ""
|
||||
};
|
||||
@@ -13,6 +14,7 @@ function getSettings() {
|
||||
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()
|
||||
});
|
||||
@@ -74,6 +76,9 @@ async function connectWS() {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -227,7 +232,7 @@ async function handleGetToken(data) {
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== "local") return;
|
||||
if (changes.routeKey || changes.serverUrl || changes.clientLabel) {
|
||||
if (changes.routeKey || changes.serverUrl || changes.apiKey || changes.clientLabel) {
|
||||
console.log("[Flow2API] Extension settings changed, reconnecting WebSocket...");
|
||||
closeSocket();
|
||||
connectWS();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Flow2API Captcha Worker",
|
||||
"version": "1.0",
|
||||
"permissions": ["activeTab", "scripting", "tabs", "storage"],
|
||||
"host_permissions": ["https://labs.google/*"],
|
||||
"options_page": "options.html",
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://labs.google/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripting"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
"https://labs.google/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const DEFAULTS = {
|
||||
routeKey: "",
|
||||
clientLabel: "",
|
||||
serverUrl: "ws://127.0.0.1:8000/captcha_ws"
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function showStatus(message, isError = false) {
|
||||
const status = $("status");
|
||||
status.textContent = message;
|
||||
status.style.color = isError ? "#b91c1c" : "#065f46";
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
chrome.storage.local.get(DEFAULTS, (stored) => {
|
||||
$("routeKey").value = stored.routeKey || "";
|
||||
$("clientLabel").value = stored.clientLabel || "";
|
||||
$("serverUrl").value = stored.serverUrl || DEFAULTS.serverUrl;
|
||||
});
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const routeKey = $("routeKey").value.trim();
|
||||
const clientLabel = $("clientLabel").value.trim();
|
||||
const serverUrl = $("serverUrl").value.trim() || DEFAULTS.serverUrl;
|
||||
|
||||
if (!serverUrl.startsWith("ws://") && !serverUrl.startsWith("wss://")) {
|
||||
showStatus("WebSocket URL 必须以 ws:// 或 wss:// 开头", true);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.set({ routeKey, clientLabel, serverUrl }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(chrome.runtime.lastError.message || "保存失败", true);
|
||||
return;
|
||||
}
|
||||
showStatus("已保存,后台连接会自动重连");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
loadSettings();
|
||||
$("saveBtn").addEventListener("click", saveSettings);
|
||||
});
|
||||
@@ -87,9 +87,12 @@
|
||||
<label for="serverUrl">WebSocket URL</label>
|
||||
<input id="serverUrl" type="text" placeholder="ws://127.0.0.1:8000/captcha_ws">
|
||||
|
||||
<label for="apiKey">Flow2API API Key</label>
|
||||
<input id="apiKey" type="password" placeholder="管理后台里的 API Key">
|
||||
|
||||
<button id="saveBtn" type="button">保存</button>
|
||||
<div class="status" id="status"></div>
|
||||
<div class="hint">管理台 token 里填写同样的 <code>Route Key</code>,比如 9223 对 9223。</div>
|
||||
<div class="hint">管理台 token 里填写同样的 <code>Route Key</code>,比如 9223 对 9223。后端会使用 API Key 校验 WebSocket 连接。</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="options.js"></script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const DEFAULT_SETTINGS = {
|
||||
serverUrl: "ws://127.0.0.1:8000/captcha_ws",
|
||||
apiKey: "",
|
||||
routeKey: "",
|
||||
clientLabel: ""
|
||||
};
|
||||
@@ -9,6 +10,7 @@ const $ = (id) => document.getElementById(id);
|
||||
function normalizeSettings(values) {
|
||||
return {
|
||||
serverUrl: (values.serverUrl || DEFAULT_SETTINGS.serverUrl).trim(),
|
||||
apiKey: (values.apiKey || "").trim(),
|
||||
routeKey: (values.routeKey || "").trim(),
|
||||
clientLabel: (values.clientLabel || "").trim()
|
||||
};
|
||||
@@ -33,6 +35,7 @@ function loadSettings() {
|
||||
chrome.storage.local.get(DEFAULT_SETTINGS, (stored) => {
|
||||
const settings = normalizeSettings(stored);
|
||||
$("serverUrl").value = settings.serverUrl;
|
||||
$("apiKey").value = settings.apiKey;
|
||||
$("routeKey").value = settings.routeKey;
|
||||
$("clientLabel").value = settings.clientLabel;
|
||||
});
|
||||
@@ -41,6 +44,7 @@ function loadSettings() {
|
||||
function saveSettings() {
|
||||
const settings = normalizeSettings({
|
||||
serverUrl: $("serverUrl").value,
|
||||
apiKey: $("apiKey").value,
|
||||
routeKey: $("routeKey").value,
|
||||
clientLabel: $("clientLabel").value
|
||||
});
|
||||
@@ -49,6 +53,10 @@ function saveSettings() {
|
||||
setStatus("WebSocket URL 必须以 ws:// 或 wss:// 开头。", true);
|
||||
return;
|
||||
}
|
||||
if (!settings.apiKey) {
|
||||
setStatus("请填写 Flow2API API Key。", true);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.set(settings, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
|
||||
@@ -9,10 +9,10 @@ import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from curl_cffi.requests import AsyncSession
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from ..core.auth import verify_api_key_flexible
|
||||
from ..core.auth import AuthManager, verify_api_key_flexible
|
||||
from ..core.logger import debug_logger
|
||||
from ..core.model_resolver import get_base_model_aliases, resolve_model_name
|
||||
from ..core.models import (
|
||||
@@ -23,7 +23,6 @@ from ..core.models import (
|
||||
)
|
||||
from ..services.generation_handler import MODEL_CONFIG, GenerationHandler
|
||||
from ..services.browser_captcha_extension import ExtensionCaptchaService
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -486,6 +485,7 @@ async def _collect_non_stream_result(
|
||||
model: str,
|
||||
prompt: str,
|
||||
images: List[bytes],
|
||||
base_url_override: Optional[str] = None,
|
||||
video_media_id: Optional[str] = None,
|
||||
) -> str:
|
||||
handler = _ensure_generation_handler()
|
||||
@@ -495,6 +495,7 @@ async def _collect_non_stream_result(
|
||||
prompt=prompt,
|
||||
images=images if images else None,
|
||||
stream=False,
|
||||
base_url_override=base_url_override,
|
||||
video_media_id=video_media_id,
|
||||
):
|
||||
result = chunk
|
||||
@@ -723,6 +724,7 @@ async def _iterate_openai_stream(
|
||||
prompt=normalized.prompt,
|
||||
images=normalized.images if normalized.images else None,
|
||||
stream=True,
|
||||
base_url_override=base_url_override,
|
||||
video_media_id=normalized.video_media_id,
|
||||
):
|
||||
if chunk.startswith("data: "):
|
||||
@@ -746,6 +748,7 @@ async def _iterate_gemini_stream(
|
||||
prompt=normalized.prompt,
|
||||
images=normalized.images if normalized.images else None,
|
||||
stream=True,
|
||||
base_url_override=base_url_override,
|
||||
video_media_id=normalized.video_media_id,
|
||||
):
|
||||
if chunk.startswith("data: "):
|
||||
@@ -874,6 +877,7 @@ async def create_chat_completion(
|
||||
normalized.model,
|
||||
normalized.prompt,
|
||||
normalized.images,
|
||||
base_url_override=request_base_url,
|
||||
video_media_id=normalized.video_media_id,
|
||||
)
|
||||
)
|
||||
@@ -907,7 +911,8 @@ async def generate_content(
|
||||
normalized.model,
|
||||
normalized.prompt,
|
||||
normalized.images,
|
||||
request_base_url,
|
||||
base_url_override=request_base_url,
|
||||
video_media_id=normalized.video_media_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -970,6 +975,20 @@ async def stream_generate_content(
|
||||
@router.websocket("/captcha_ws")
|
||||
async def captcha_websocket_endpoint(websocket: WebSocket):
|
||||
from ..core.logger import debug_logger
|
||||
api_key = (
|
||||
websocket.query_params.get("key")
|
||||
or websocket.query_params.get("api_key")
|
||||
or websocket.headers.get("x-goog-api-key")
|
||||
or ""
|
||||
).strip()
|
||||
authorization = (websocket.headers.get("authorization") or "").strip()
|
||||
if authorization.lower().startswith("bearer "):
|
||||
api_key = authorization[7:].strip()
|
||||
|
||||
if not api_key or not AuthManager.verify_api_key(api_key):
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
|
||||
service = await ExtensionCaptchaService.get_instance()
|
||||
await service.connect(websocket)
|
||||
try:
|
||||
|
||||
@@ -12,8 +12,11 @@ class Config:
|
||||
self._admin_password: Optional[str] = None
|
||||
|
||||
def _load_config(self) -> Dict[str, Any]:
|
||||
"""Load configuration from setting.toml"""
|
||||
config_path = Path(__file__).parent.parent.parent / "config" / "setting.toml"
|
||||
"""Load configuration from setting.toml, falling back to the example file."""
|
||||
config_dir = Path(__file__).parent.parent.parent / "config"
|
||||
config_path = config_dir / "setting.toml"
|
||||
if not config_path.exists():
|
||||
config_path = config_dir / "setting_example.toml"
|
||||
with open(config_path, "rb") as f:
|
||||
return tomli.load(f)
|
||||
|
||||
|
||||
@@ -408,6 +408,7 @@ class Database:
|
||||
("image_concurrency", "INTEGER DEFAULT -1"),
|
||||
("video_concurrency", "INTEGER DEFAULT -1"),
|
||||
("captcha_proxy_url", "TEXT"), # token级打码代理
|
||||
("extension_route_key", "TEXT"), # extension 模式路由键
|
||||
("ban_reason", "TEXT"), # 禁用原因
|
||||
("banned_at", "TIMESTAMP"), # 禁用时间
|
||||
]
|
||||
@@ -567,6 +568,7 @@ class Database:
|
||||
image_concurrency INTEGER DEFAULT -1,
|
||||
video_concurrency INTEGER DEFAULT -1,
|
||||
captcha_proxy_url TEXT,
|
||||
extension_route_key TEXT,
|
||||
ban_reason TEXT,
|
||||
banned_at TIMESTAMP
|
||||
)
|
||||
@@ -845,13 +847,15 @@ class Database:
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO tokens (st, at, at_expires, email, name, remark, is_active,
|
||||
credits, user_paygate_tier, current_project_id, current_project_name,
|
||||
image_enabled, video_enabled, image_concurrency, video_concurrency, captcha_proxy_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
image_enabled, video_enabled, image_concurrency, video_concurrency,
|
||||
captcha_proxy_url, extension_route_key)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (token.st, token.at, token.at_expires, token.email, token.name, token.remark,
|
||||
token.is_active, token.credits, token.user_paygate_tier,
|
||||
token.current_project_id, token.current_project_name,
|
||||
token.image_enabled, token.video_enabled,
|
||||
token.image_concurrency, token.video_concurrency, token.captcha_proxy_url))
|
||||
token.image_concurrency, token.video_concurrency,
|
||||
token.captcha_proxy_url, token.extension_route_key))
|
||||
await db.commit()
|
||||
token_id = cursor.lastrowid
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class Token(BaseModel):
|
||||
|
||||
# 打码代理(token 级,可覆盖全局浏览器打码代理)
|
||||
captcha_proxy_url: Optional[str] = None
|
||||
extension_route_key: Optional[str] = None
|
||||
|
||||
# 429禁用相关
|
||||
ban_reason: Optional[str] = None # 禁用原因: "429_rate_limit" 或 None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -24,7 +25,7 @@ class ExtensionCaptchaService:
|
||||
def __init__(self, db=None):
|
||||
self.db = db
|
||||
self.active_connections: list[ExtensionConnection] = []
|
||||
self.pending_requests: dict[str, asyncio.Future] = {}
|
||||
self.pending_requests: dict[str, tuple[asyncio.Future, WebSocket]] = {}
|
||||
|
||||
@classmethod
|
||||
async def get_instance(cls, db=None) -> "ExtensionCaptchaService":
|
||||
@@ -143,7 +144,10 @@ class ExtensionCaptchaService:
|
||||
|
||||
req_id = payload.get("req_id")
|
||||
if req_id and req_id in self.pending_requests:
|
||||
future = self.pending_requests[req_id]
|
||||
future, owner_websocket = self.pending_requests[req_id]
|
||||
if websocket is not owner_websocket:
|
||||
debug_logger.log_warning(f"[Extension Captcha] Ignoring response from non-owner connection: {req_id}")
|
||||
return
|
||||
if not future.done():
|
||||
future.set_result(payload)
|
||||
except Exception as e:
|
||||
@@ -169,9 +173,9 @@ class ExtensionCaptchaService:
|
||||
f"Available route keys: {available}"
|
||||
)
|
||||
|
||||
req_id = f"req_{int(time.time() * 1000)}_{id(self)}"
|
||||
future = asyncio.Future()
|
||||
self.pending_requests[req_id] = future
|
||||
req_id = f"req_{uuid.uuid4().hex}"
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
self.pending_requests[req_id] = (future, conn.websocket)
|
||||
|
||||
request_data = {
|
||||
"type": "get_token",
|
||||
@@ -194,16 +198,13 @@ class ExtensionCaptchaService:
|
||||
|
||||
error_msg = result.get("error")
|
||||
debug_logger.log_error(f"[Extension Captcha] Error from extension: {error_msg}")
|
||||
print(f"!!! EXTENSION ERROR: {error_msg} !!!")
|
||||
return None
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
debug_logger.log_error(f"[Extension Captcha] Timeout waiting for token (req_id: {req_id})")
|
||||
print("!!! EXTENSION TIMEOUT !!!")
|
||||
return None
|
||||
except Exception as e:
|
||||
debug_logger.log_error(f"[Extension Captcha] Communication error: {e}")
|
||||
print(f"!!! EXTENSION COMM ERROR: {e} !!!")
|
||||
return None
|
||||
finally:
|
||||
self.pending_requests.pop(req_id, None)
|
||||
|
||||
@@ -213,14 +213,6 @@ class FlowClient:
|
||||
headers["sec-ch-ua-platform"] = "\"Windows\""
|
||||
headers["sec-ch-ua-mobile"] = "?0"
|
||||
|
||||
if 'aisandbox' in url:
|
||||
print(f"[DEBUG-DEEP] API REQUEST to: {url[:80]}")
|
||||
print(f"[DEBUG-DEEP] fingerprint: {fingerprint}")
|
||||
print(f"[DEBUG-DEEP] proxy_url: {proxy_url}")
|
||||
print(f"[DEBUG-DEEP] UA: {headers.get('User-Agent', '')[:100]}")
|
||||
print(f"[DEBUG-DEEP] sec-ch-ua-platform: {headers.get('sec-ch-ua-platform', 'NOT SET')}")
|
||||
print(f"[DEBUG-DEEP] sec-ch-ua-mobile: {headers.get('sec-ch-ua-mobile', 'NOT SET')}")
|
||||
|
||||
# Log request
|
||||
if config.debug_enabled:
|
||||
if isinstance(fingerprint, dict):
|
||||
@@ -1946,7 +1938,6 @@ class FlowClient:
|
||||
]
|
||||
}
|
||||
|
||||
print(f"[CONCAT] 提交拼接: original={original_media_id}, extend={extend_media_id}", flush=True)
|
||||
debug_logger.log_info(f"[CONCAT] 提交拼接任务: original={original_media_id[:12]}..., extend={extend_media_id[:12]}...")
|
||||
|
||||
result = await self._make_request(
|
||||
@@ -1956,7 +1947,7 @@ class FlowClient:
|
||||
use_at=True,
|
||||
at_token=at
|
||||
)
|
||||
print(f"[CONCAT] 提交结果: {json.dumps(result, ensure_ascii=False)[:500]}", flush=True)
|
||||
debug_logger.log_info(f"[CONCAT] 拼接任务已提交: {json.dumps(result, ensure_ascii=False)[:300]}")
|
||||
return result
|
||||
|
||||
async def poll_concatenation_status(
|
||||
@@ -2006,11 +1997,14 @@ class FlowClient:
|
||||
ev_len = len(encoded_video) if encoded_video else 0
|
||||
elapsed = int(time.time() - start_time)
|
||||
all_keys = list(result.keys())
|
||||
print(f"[CONCAT] 状态: {status}, outputUri: {'有:'+output_uri[:40] if output_uri else 'N/A'}, encodedVideo: {ev_len} chars, elapsed: {elapsed}s, keys: {all_keys}", flush=True)
|
||||
debug_logger.log_info(
|
||||
f"[CONCAT] 状态: {status}, outputUri={'yes' if output_uri else 'no'}, "
|
||||
f"encodedVideo={ev_len} chars, elapsed={elapsed}s, keys={all_keys}"
|
||||
)
|
||||
|
||||
# 优先检查 outputUri
|
||||
if output_uri:
|
||||
print(f"[CONCAT] 拼接完成 (outputUri): {output_uri[:120]}", flush=True)
|
||||
debug_logger.log_info(f"[CONCAT] 拼接完成 (outputUri): {output_uri[:120]}")
|
||||
return result
|
||||
|
||||
# Google API 返回 encodedVideo(base64 编码的 MP4)而不是 outputUri
|
||||
@@ -2030,26 +2024,26 @@ class FlowClient:
|
||||
|
||||
# 构造 URL:FastAPI 挂载了 /tmp -> /app/tmp/
|
||||
serve_url = f"/tmp/{video_filename}"
|
||||
print(f"[CONCAT] 拼接完成 (encodedVideo): 保存 {len(video_bytes)} bytes -> {serve_url}", flush=True)
|
||||
debug_logger.log_info(f"[CONCAT] 拼接完成 (encodedVideo): 保存 {len(video_bytes)} bytes -> {serve_url}")
|
||||
|
||||
result["outputUri"] = serve_url
|
||||
result["local_file"] = save_path
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"[CONCAT] 解码 encodedVideo 失败: {e}", flush=True)
|
||||
debug_logger.log_error(f"[CONCAT] 解码 encodedVideo 失败: {e}")
|
||||
raise Exception(f"解码拼接视频失败: {e}")
|
||||
|
||||
# SUCCESSFUL but neither outputUri nor encodedVideo
|
||||
if "SUCCESSFUL" in status:
|
||||
print(f"[CONCAT] SUCCESSFUL 但无 outputUri/encodedVideo,完整响应: {json.dumps(result, ensure_ascii=False)[:500]}", flush=True)
|
||||
|
||||
debug_logger.log_warning(f"[CONCAT] SUCCESSFUL 但无 outputUri/encodedVideo: {json.dumps(result, ensure_ascii=False)[:300]}")
|
||||
|
||||
if "FAILED" in status or "ERROR" in status:
|
||||
print(f"[CONCAT] 失败: {status}, 响应: {json.dumps(result, ensure_ascii=False)[:300]}", flush=True)
|
||||
debug_logger.log_error(f"[CONCAT] 失败: {status}, 响应: {json.dumps(result, ensure_ascii=False)[:300]}")
|
||||
raise Exception(f"视频拼接失败: {status}")
|
||||
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
print(f"[CONCAT] 超时 ({timeout}s),放弃拼接", flush=True)
|
||||
debug_logger.log_error(f"[CONCAT] 超时 ({timeout}s),放弃拼接")
|
||||
raise Exception(f"视频拼接超时 ({timeout}s)")
|
||||
|
||||
# ========== 视频放大 (Video Upsampler) ==========
|
||||
@@ -2662,13 +2656,12 @@ class FlowClient:
|
||||
- 其他模式: browser_id 为 None
|
||||
"""
|
||||
captcha_method = config.captcha_method
|
||||
print(f"[DEBUG] _get_recaptcha_token called: captcha_method={captcha_method}, project_id={project_id}, action={action}")
|
||||
debug_logger.log_info(f"[reCAPTCHA] 开始获取 token: method={captcha_method}, project_id={project_id}, action={action}")
|
||||
|
||||
if captcha_method == "extension":
|
||||
try:
|
||||
from .browser_captcha_extension import ExtensionCaptchaService
|
||||
service = await ExtensionCaptchaService.get_instance(self.db)
|
||||
print(f"[DEBUG-EXTENSION] get_token requested, active_connections length: {len(service.active_connections)}")
|
||||
extension_timeout = 45 if action == "VIDEO_GENERATION" else 25
|
||||
token = await service.get_token(
|
||||
project_id,
|
||||
@@ -2680,7 +2673,6 @@ class FlowClient:
|
||||
return token, None
|
||||
except Exception as e:
|
||||
debug_logger.log_error(f"[reCAPTCHA Extension] 错误: {str(e)}")
|
||||
print(f"[DEBUG-EXTENSION] EXCEPTION IN get_token: {e}")
|
||||
self._set_request_fingerprint(None)
|
||||
return None, None
|
||||
|
||||
@@ -2695,8 +2687,6 @@ class FlowClient:
|
||||
token = await service.get_token(project_id, action)
|
||||
debug_logger.log_info(f"[reCAPTCHA] get_token 返回: {token[:50] if token else None}...")
|
||||
fingerprint = service.get_last_fingerprint() if token else None
|
||||
print(f"[DEBUG-DEEP] personal token obtained: {bool(token)}, token_prefix={str(token)[:40] if token else 'None'}")
|
||||
print(f"[DEBUG-DEEP] personal fingerprint: {fingerprint}")
|
||||
self._set_request_fingerprint(fingerprint if token else None)
|
||||
return token, None
|
||||
except RuntimeError as e:
|
||||
@@ -2772,11 +2762,10 @@ class FlowClient:
|
||||
proxy_url = await self.proxy_manager.get_request_proxy_url()
|
||||
if proxy_url:
|
||||
self._set_request_fingerprint({"proxy_url": proxy_url})
|
||||
print(f"[DEBUG] API captcha using proxy: {proxy_url[:60]}...")
|
||||
else:
|
||||
self._set_request_fingerprint(None)
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Failed to get proxy for API captcha: {e}")
|
||||
debug_logger.log_warning(f"[reCAPTCHA] Failed to get proxy for API captcha: {e}")
|
||||
self._set_request_fingerprint(None)
|
||||
else:
|
||||
self._set_request_fingerprint(None)
|
||||
@@ -2832,22 +2821,15 @@ class FlowClient:
|
||||
if self.proxy_manager:
|
||||
try:
|
||||
proxy_url = await self.proxy_manager.get_request_proxy_url()
|
||||
print(f"[DEBUG] Got proxy_url: {proxy_url[:60] if proxy_url else None}")
|
||||
if proxy_url:
|
||||
if proxy_url.startswith("socks5://"):
|
||||
# curl_cffi 对 SOCKS5 使用 proxy 参数
|
||||
proxy = proxy_url
|
||||
print(f"[DEBUG] Using SOCKS5 proxy for CapSolver: {proxy[:60]}...")
|
||||
else:
|
||||
# HTTP/HTTPS 代理使用 proxies 字典
|
||||
proxies = {"http": proxy_url, "https": proxy_url}
|
||||
print(f"[DEBUG] Using HTTP proxy for CapSolver: {proxy_url[:60]}...")
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Failed to get proxy: {e}")
|
||||
debug_logger.log_warning(f"[reCAPTCHA {method}] Failed to get proxy: {e}")
|
||||
|
||||
print(f"[DEBUG] CapSolver request: method={method}, project_id={project_id}, action={action}")
|
||||
print(f"[DEBUG] CapSolver API Key: {client_key[:20]}...")
|
||||
|
||||
async with AsyncSession() as session:
|
||||
create_url = f"{base_url}/createTask"
|
||||
@@ -2861,21 +2843,15 @@ class FlowClient:
|
||||
}
|
||||
}
|
||||
|
||||
# 根据代理类型使用不同参数
|
||||
print(f"[DEBUG] Sending createTask request to {create_url}")
|
||||
if proxy:
|
||||
print(f"[DEBUG] Using proxy param: {proxy[:60]}...")
|
||||
result = await session.post(create_url, json=create_data, impersonate="chrome124", proxy=proxy)
|
||||
else:
|
||||
print(f"[DEBUG] Using proxies dict or no proxy")
|
||||
result = await session.post(create_url, json=create_data, impersonate="chrome124", proxies=proxies)
|
||||
|
||||
print(f"[DEBUG] createTask response status: {result.status_code}")
|
||||
|
||||
debug_logger.log_info(f"[reCAPTCHA {method}] createTask response status: {result.status_code}")
|
||||
result_json = result.json()
|
||||
task_id = result_json.get('taskId')
|
||||
|
||||
print(f"[DEBUG] createTask response: {result_json}")
|
||||
debug_logger.log_info(f"[reCAPTCHA {method}] created task_id: {task_id}, response: {result_json}")
|
||||
|
||||
if not task_id:
|
||||
@@ -2896,29 +2872,21 @@ class FlowClient:
|
||||
result = await session.post(get_url, json=get_data, impersonate="chrome124", proxies=proxies)
|
||||
result_json = result.json()
|
||||
|
||||
print(f"[DEBUG] getTaskResult polling #{i+1}: status={result_json.get('status')}, response={result_json}")
|
||||
debug_logger.log_info(f"[reCAPTCHA {method}] polling #{i+1}: {result_json}")
|
||||
|
||||
status = result_json.get('status')
|
||||
if status == 'ready':
|
||||
solution = result_json.get('solution', {})
|
||||
response = solution.get('gRecaptchaResponse')
|
||||
print(f"[DEBUG] Got solution: {solution}")
|
||||
print(f"[DEBUG] Got gRecaptchaResponse: {response[:50] if response else None}")
|
||||
if response:
|
||||
print(f"[DEBUG] Token获取成功")
|
||||
debug_logger.log_info(f"[reCAPTCHA {method}] Token获取成功")
|
||||
return response
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
print(f"[DEBUG] Timeout waiting for token after 40 retries")
|
||||
debug_logger.log_error(f"[reCAPTCHA {method}] Timeout waiting for token")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Exception in _get_api_captcha_token: {type(e).__name__}: {str(e)}")
|
||||
debug_logger.log_error(f"[reCAPTCHA {method}] error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
@@ -818,6 +818,7 @@ class GenerationHandler:
|
||||
prompt: str,
|
||||
images: Optional[List[bytes]] = None,
|
||||
stream: bool = False,
|
||||
base_url_override: Optional[str] = None,
|
||||
video_media_id: Optional[str] = None,
|
||||
) -> AsyncGenerator:
|
||||
"""统一生成入口
|
||||
@@ -1688,7 +1689,6 @@ class GenerationHandler:
|
||||
debug_logger.log_info(f"[EXTEND] 续写视频: {video_media_id}")
|
||||
if stream:
|
||||
yield self._create_stream_chunk(f"视频续写任务提交中,源视频: {video_media_id[:8]}...\n")
|
||||
print(f"[EXTEND-DEBUG] Calling generate_video_extend with video_media_id={video_media_id}")
|
||||
result = await self.flow_client.generate_video_extend(
|
||||
at=token.at,
|
||||
project_id=project_id,
|
||||
@@ -1755,7 +1755,17 @@ class GenerationHandler:
|
||||
|
||||
# 如果是 extend,传入源视频 media_id 用于后续拼接
|
||||
extend_source_id = video_media_id if video_type == "extend" else None
|
||||
async for chunk in self._poll_video_result(token, project_id, operations, stream, upsample_config, generation_result, request_log_state, extend_source_media_id=extend_source_id):
|
||||
async for chunk in self._poll_video_result(
|
||||
token,
|
||||
project_id,
|
||||
operations,
|
||||
stream,
|
||||
upsample_config,
|
||||
generation_result,
|
||||
response_state,
|
||||
request_log_state,
|
||||
extend_source_media_id=extend_source_id,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
finally:
|
||||
@@ -1769,6 +1779,7 @@ class GenerationHandler:
|
||||
stream: bool,
|
||||
upsample_config: Optional[Dict] = None,
|
||||
generation_result: Optional[Dict[str, Any]] = None,
|
||||
response_state: Optional[Dict[str, Any]] = None,
|
||||
request_log_state: Optional[Dict[str, Any]] = None,
|
||||
extend_source_media_id: Optional[str] = None,
|
||||
) -> AsyncGenerator:
|
||||
@@ -1826,7 +1837,6 @@ class GenerationHandler:
|
||||
import re as _re
|
||||
_uuid_match = _re.search(r'/video/([0-9a-f-]{36})', video_url or '')
|
||||
video_media_id = _uuid_match.group(1) if _uuid_match else video_info.get("mediaGenerationId", "")
|
||||
print(f"[POLL-DEBUG] fifeUrl={str(video_url)[:80]}, video_media_id={video_media_id}")
|
||||
aspect_ratio = video_info.get("aspectRatio", "VIDEO_ASPECT_RATIO_LANDSCAPE")
|
||||
|
||||
if not video_url:
|
||||
@@ -1862,7 +1872,14 @@ class GenerationHandler:
|
||||
|
||||
# 递归轮询放大结果(不再放大)
|
||||
async for chunk in self._poll_video_result(
|
||||
token, project_id, upsample_operations, stream, None, generation_result, response_state, request_log_state
|
||||
token,
|
||||
project_id,
|
||||
upsample_operations,
|
||||
stream,
|
||||
None,
|
||||
generation_result,
|
||||
response_state,
|
||||
request_log_state,
|
||||
):
|
||||
yield chunk
|
||||
return
|
||||
@@ -1875,12 +1892,10 @@ class GenerationHandler:
|
||||
yield self._create_stream_chunk(f"⚠️ 放大失败: {str(e)},返回原始视频\n")
|
||||
|
||||
# ========== Extend 视频拼接 ==========
|
||||
print(f"[CONCAT-DEBUG] extend_source_media_id={extend_source_media_id}, video_media_id={video_media_id}", flush=True)
|
||||
if extend_source_media_id and video_media_id:
|
||||
try:
|
||||
if stream:
|
||||
yield self._create_stream_chunk("\n视频续写完成,正在拼接完整视频...\n")
|
||||
print(f"[CONCAT] 开始拼接: original={extend_source_media_id[:40]}..., extend={video_media_id[:40]}...", flush=True)
|
||||
debug_logger.log_info(f"[CONCAT] 开始拼接: original={extend_source_media_id[:12]}..., extend={video_media_id[:12]}...")
|
||||
|
||||
# 提交拼接任务
|
||||
@@ -1926,9 +1941,8 @@ class GenerationHandler:
|
||||
yield self._create_stream_chunk("⚠️ 拼接任务创建失败,返回续写片段\n")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[CONCAT] ❌ 拼接异常: {str(e)}", flush=True)
|
||||
print(f"[CONCAT] traceback: {traceback.format_exc()}", flush=True)
|
||||
debug_logger.log_error(f"[CONCAT] 拼接失败: {str(e)}")
|
||||
debug_logger.log_error(f"[CONCAT] traceback: {traceback.format_exc()}")
|
||||
if stream:
|
||||
yield self._create_stream_chunk(f"⚠️ 拼接失败: {str(e)},返回续写片段\n")
|
||||
# 拼接失败不影响返回,继续使用 extend 片段的 URL
|
||||
|
||||
@@ -209,7 +209,8 @@ class TokenManager:
|
||||
video_enabled: bool = True,
|
||||
image_concurrency: int = -1,
|
||||
video_concurrency: int = -1,
|
||||
captcha_proxy_url: Optional[str] = None
|
||||
captcha_proxy_url: Optional[str] = None,
|
||||
extension_route_key: Optional[str] = None,
|
||||
) -> Token:
|
||||
"""Add a new token and prepare its pooled projects."""
|
||||
existing_token = await self.db.get_token_by_st(st)
|
||||
@@ -284,7 +285,8 @@ class TokenManager:
|
||||
video_enabled=video_enabled,
|
||||
image_concurrency=image_concurrency,
|
||||
video_concurrency=video_concurrency,
|
||||
captcha_proxy_url=captcha_proxy_url
|
||||
captcha_proxy_url=captcha_proxy_url,
|
||||
extension_route_key=extension_route_key,
|
||||
)
|
||||
|
||||
token_id = await self.db.add_token(token)
|
||||
@@ -314,7 +316,8 @@ class TokenManager:
|
||||
video_enabled: Optional[bool] = None,
|
||||
image_concurrency: Optional[int] = None,
|
||||
video_concurrency: Optional[int] = None,
|
||||
captcha_proxy_url: Optional[str] = None
|
||||
captcha_proxy_url: Optional[str] = None,
|
||||
extension_route_key: Optional[str] = None,
|
||||
):
|
||||
"""Update token (支持修改project_id和project_name)
|
||||
|
||||
@@ -344,6 +347,8 @@ class TokenManager:
|
||||
update_fields["video_concurrency"] = video_concurrency
|
||||
if captcha_proxy_url is not None:
|
||||
update_fields["captcha_proxy_url"] = captcha_proxy_url
|
||||
if extension_route_key is not None:
|
||||
update_fields["extension_route_key"] = extension_route_key
|
||||
|
||||
# 检查token是否因429被禁用,如果是且未过期,则清空429状态
|
||||
token = await self.db.get_token(token_id)
|
||||
|
||||
135
test.py
135
test.py
@@ -1,135 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# ── 配置 ──────────────────────────────────────────────
|
||||
IMAGE_PATH = Path(__file__).parent / "cat.jpg"
|
||||
API_URL = "http://localhost:8000/v1/chat/completions"
|
||||
API_KEY = "han1234"
|
||||
MODEL = "gemini-3.1-flash-image-landscape"
|
||||
PROMPT = "将这张图片变成水彩画风格"
|
||||
OUTPUT = Path(__file__).parent / "cat_watercolor.jpg"
|
||||
# ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def read_image_base64(path: Path) -> str:
|
||||
with open(path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode()
|
||||
|
||||
|
||||
def call_api(b64_image: str) -> str:
|
||||
"""发送请求,收集所有 SSE chunk,返回拼接后的完整 content。"""
|
||||
payload = json.dumps({
|
||||
"model": MODEL,
|
||||
"stream": True,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": PROMPT},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_image}"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
API_URL,
|
||||
data=payload,
|
||||
headers={
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
content = ""
|
||||
reasoning = ""
|
||||
with urllib.request.urlopen(req, timeout=300) as resp:
|
||||
for raw_line in resp:
|
||||
line = raw_line.decode("utf-8", errors="replace").strip()
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
data_str = line[5:].strip()
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# 检查错误
|
||||
if "error" in chunk:
|
||||
raise RuntimeError(chunk["error"].get("message", str(chunk["error"])))
|
||||
|
||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
if delta.get("reasoning_content"):
|
||||
r = delta["reasoning_content"]
|
||||
reasoning += r
|
||||
print(r, end="", flush=True)
|
||||
if delta.get("content"):
|
||||
content += delta["content"]
|
||||
|
||||
print() # 换行
|
||||
return content
|
||||
|
||||
|
||||
def extract_image_url(content: str) -> Optional[str]:
|
||||
"""从 markdown 或纯文本中提取图片 URL。"""
|
||||
#  或直接 URL
|
||||
patterns = [
|
||||
r"!\[.*?\]\((https?://\S+?)\)",
|
||||
r"(https?://\S+\.(?:jpg|jpeg|png|webp|gif)(?:\?\S*)?)",
|
||||
r"(https?://\S+)",
|
||||
]
|
||||
for pat in patterns:
|
||||
m = re.search(pat, content)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def download_image(url: str, dest: Path):
|
||||
print(f"📥 下载图片: {url}")
|
||||
urllib.request.urlretrieve(url, dest)
|
||||
print(f"✅ 已保存到: {dest}")
|
||||
|
||||
|
||||
def main():
|
||||
if not IMAGE_PATH.exists():
|
||||
raise FileNotFoundError(f"找不到输入图片: {IMAGE_PATH}")
|
||||
|
||||
print(f"📷 读取图片: {IMAGE_PATH}")
|
||||
b64 = read_image_base64(IMAGE_PATH)
|
||||
|
||||
print(f"🚀 发送请求到 {API_URL} ...")
|
||||
print("-" * 50)
|
||||
content = call_api(b64)
|
||||
print("-" * 50)
|
||||
|
||||
if not content:
|
||||
print("⚠️ 响应 content 为空,可能图片已在 reasoning_content 中描述,请查看上方日志。")
|
||||
return
|
||||
|
||||
print(f"\n📝 响应内容:\n{content}\n")
|
||||
|
||||
url = extract_image_url(content)
|
||||
if url:
|
||||
download_image(url, OUTPUT)
|
||||
else:
|
||||
# 尝试 base64 内嵌
|
||||
b64_match = re.search(r"data:image/[^;]+;base64,([A-Za-z0-9+/=]+)", content)
|
||||
if b64_match:
|
||||
img_data = base64.b64decode(b64_match.group(1))
|
||||
OUTPUT.write_bytes(img_data)
|
||||
print(f"✅ base64 图片已保存到: {OUTPUT}")
|
||||
else:
|
||||
print("⚠️ 未能从响应中提取图片 URL,请手动查看上方 content。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -62,7 +62,7 @@ class VeoLiteFlowClientTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_generate_video_text_uses_v2_payload_for_lite(self):
|
||||
captured = {}
|
||||
|
||||
async def fake_make_request(method, url, json_data, use_at, at_token):
|
||||
async def fake_make_request(method, url, json_data, use_at, at_token, **kwargs):
|
||||
captured["url"] = url
|
||||
captured["json_data"] = json_data
|
||||
return {"operations": [{"operation": {"name": "task-1"}}]}
|
||||
@@ -92,7 +92,7 @@ class VeoLiteFlowClientTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_generate_video_start_end_uses_v2_payload_for_interpolation_lite(self):
|
||||
captured = {}
|
||||
|
||||
async def fake_make_request(method, url, json_data, use_at, at_token):
|
||||
async def fake_make_request(method, url, json_data, use_at, at_token, **kwargs):
|
||||
captured["url"] = url
|
||||
captured["json_data"] = json_data
|
||||
return {"operations": [{"operation": {"name": "task-2"}}]}
|
||||
|
||||
Reference in New Issue
Block a user