|
|
@@ -452,6 +452,141 @@ class BasePublisher(ABC):
|
|
|
"notes": f"AI 分析异常: {e}",
|
|
|
}
|
|
|
|
|
|
+ async def sync_cookies_to_node(self, cookies: list) -> bool:
|
|
|
+ import os
|
|
|
+ import json
|
|
|
+ import requests
|
|
|
+
|
|
|
+ if not self.user_id or not self.publish_account_id:
|
|
|
+ return False
|
|
|
+
|
|
|
+ async def ai_suggest_playwright_selector(self, goal: str, screenshot_base64: str = None) -> dict:
|
|
|
+ import os
|
|
|
+ import requests
|
|
|
+ import json
|
|
|
+ import re
|
|
|
+
|
|
|
+ try:
|
|
|
+ if not screenshot_base64:
|
|
|
+ screenshot_base64 = await self.capture_screenshot()
|
|
|
+
|
|
|
+ if not screenshot_base64:
|
|
|
+ return {"has_selector": False, "selector": "", "confidence": 0, "notes": "无法获取截图"}
|
|
|
+
|
|
|
+ ai_api_key = os.environ.get('DASHSCOPE_API_KEY', '')
|
|
|
+ ai_base_url = os.environ.get('DASHSCOPE_BASE_URL', 'https://dashscope.aliyuncs.com/compatible-mode/v1')
|
|
|
+ ai_vision_model = os.environ.get('AI_VISION_MODEL', 'qwen-vl-plus')
|
|
|
+
|
|
|
+ if not ai_api_key:
|
|
|
+ return {"has_selector": False, "selector": "", "confidence": 0, "notes": "未配置 AI API Key"}
|
|
|
+
|
|
|
+ prompt = f"""请分析这张网页截图,给出一个 Playwright Python 可用的 selector(用于 page.locator(selector))来完成目标操作。
|
|
|
+
|
|
|
+目标:{goal}
|
|
|
+
|
|
|
+要求:
|
|
|
+1) selector 尽量稳定(优先 role/text/aria,其次 class,避免过度依赖随机 class)
|
|
|
+2) selector 必须是 Playwright 支持的选择器语法(如:text="发布"、button:has-text("发布")、[role="button"]:has-text("发布") 等)
|
|
|
+3) 只返回一个最优 selector
|
|
|
+
|
|
|
+以 JSON 返回:
|
|
|
+```json
|
|
|
+{{
|
|
|
+ "has_selector": true,
|
|
|
+ "selector": "button:has-text(\\"发布\\")",
|
|
|
+ "confidence": 0-100,
|
|
|
+ "notes": "你依据的页面证据"
|
|
|
+}}
|
|
|
+```"""
|
|
|
+
|
|
|
+ headers = {
|
|
|
+ 'Authorization': f'Bearer {ai_api_key}',
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+
|
|
|
+ payload = {
|
|
|
+ "model": ai_vision_model,
|
|
|
+ "messages": [
|
|
|
+ {
|
|
|
+ "role": "user",
|
|
|
+ "content": [
|
|
|
+ {
|
|
|
+ "type": "image_url",
|
|
|
+ "image_url": {
|
|
|
+ "url": f"data:image/jpeg;base64,{screenshot_base64}"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "type": "text",
|
|
|
+ "text": prompt
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "max_tokens": 300
|
|
|
+ }
|
|
|
+
|
|
|
+ response = requests.post(
|
|
|
+ f"{ai_base_url}/chat/completions",
|
|
|
+ headers=headers,
|
|
|
+ json=payload,
|
|
|
+ timeout=30
|
|
|
+ )
|
|
|
+ if response.status_code != 200:
|
|
|
+ return {"has_selector": False, "selector": "", "confidence": 0, "notes": f"AI API 错误 {response.status_code}"}
|
|
|
+
|
|
|
+ result = response.json()
|
|
|
+ ai_response = result.get('choices', [{}])[0].get('message', {}).get('content', '')
|
|
|
+
|
|
|
+ json_match = re.search(r'```json\\s*([\\s\\S]*?)\\s*```', ai_response)
|
|
|
+ if json_match:
|
|
|
+ json_str = json_match.group(1)
|
|
|
+ else:
|
|
|
+ json_match = re.search(r'\\{[\\s\\S]*\\}', ai_response)
|
|
|
+ json_str = json_match.group(0) if json_match else '{}'
|
|
|
+
|
|
|
+ try:
|
|
|
+ data = json.loads(json_str)
|
|
|
+ except Exception:
|
|
|
+ data = {}
|
|
|
+
|
|
|
+ selector = str(data.get("selector", "") or "").strip()
|
|
|
+ has_selector = bool(data.get("has_selector", False)) and bool(selector)
|
|
|
+ confidence = int(data.get("confidence", 0) or 0)
|
|
|
+ notes = str(data.get("notes", "") or "")
|
|
|
+
|
|
|
+ if not has_selector:
|
|
|
+ return {"has_selector": False, "selector": "", "confidence": confidence, "notes": notes or "未给出 selector"}
|
|
|
+
|
|
|
+ return {"has_selector": True, "selector": selector, "confidence": confidence, "notes": notes}
|
|
|
+ except Exception as e:
|
|
|
+ return {"has_selector": False, "selector": "", "confidence": 0, "notes": f"AI selector 异常: {e}"}
|
|
|
+
|
|
|
+ node_api_url = os.environ.get('NODEJS_API_URL', 'http://localhost:3000').rstrip('/')
|
|
|
+ internal_api_key = os.environ.get('INTERNAL_API_KEY', 'internal-api-key-default')
|
|
|
+
|
|
|
+ try:
|
|
|
+ payload = {
|
|
|
+ "user_id": int(self.user_id),
|
|
|
+ "account_id": int(self.publish_account_id),
|
|
|
+ "cookies": json.dumps(cookies, ensure_ascii=False),
|
|
|
+ }
|
|
|
+ resp = requests.post(
|
|
|
+ f"{node_api_url}/api/internal/accounts/update-cookies",
|
|
|
+ headers={
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ "X-Internal-API-Key": internal_api_key,
|
|
|
+ },
|
|
|
+ json=payload,
|
|
|
+ timeout=30,
|
|
|
+ )
|
|
|
+ if resp.status_code >= 400:
|
|
|
+ return False
|
|
|
+ data = resp.json() if resp.content else {}
|
|
|
+ return bool(data.get("success", True))
|
|
|
+ except Exception:
|
|
|
+ return False
|
|
|
+
|
|
|
async def ai_check_captcha(self, screenshot_base64: str = None) -> dict:
|
|
|
"""
|
|
|
使用 AI 分析截图检测验证码
|