ownapi 1.0.0__py3-none-any.whl
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.
- cli.py +1017 -0
- compiler.py +948 -0
- core/__init__.py +19 -0
- core/api_keys.py +523 -0
- core/auto_repair.py +682 -0
- core/base_client.py +165 -0
- core/consent.py +169 -0
- core/form_automation.py +1044 -0
- core/hitl_queue.py +886 -0
- core/onboarding.py +1457 -0
- core/token_store.py +223 -0
- healer.py +2074 -0
- master_mcp.py +193 -0
- ownapi/__init__.py +94 -0
- ownapi/cli.py +309 -0
- ownapi/sdk.py +436 -0
- ownapi-1.0.0.dist-info/METADATA +287 -0
- ownapi-1.0.0.dist-info/RECORD +62 -0
- ownapi-1.0.0.dist-info/WHEEL +5 -0
- ownapi-1.0.0.dist-info/entry_points.txt +4 -0
- ownapi-1.0.0.dist-info/top_level.txt +10 -0
- recorder.py +486 -0
- registry/coingecko/__init__.py +5 -0
- registry/coingecko/client.py +84 -0
- registry/coingecko/schema.json +23 -0
- registry/edevlet/__init__.py +5 -0
- registry/edevlet/client.py +54 -0
- registry/edevlet/schema.json +14 -0
- registry/enuygun/__init__.py +5 -0
- registry/enuygun/client.py +78 -0
- registry/enuygun/schema.json +13 -0
- registry/github_trending/__init__.py +5 -0
- registry/github_trending/client.py +90 -0
- registry/github_trending/schema.json +31 -0
- registry/hackernews/__init__.py +5 -0
- registry/hackernews/client.py +90 -0
- registry/hackernews/schema.json +38 -0
- registry/hepsiburada/__init__.py +5 -0
- registry/hepsiburada/client.py +32 -0
- registry/hepsiburada/schema.json +23 -0
- registry/linkedin_jobs/__init__.py +5 -0
- registry/linkedin_jobs/client.py +241 -0
- registry/linkedin_jobs/schema.json +46 -0
- registry/pegasus/__init__.py +5 -0
- registry/pegasus/client.py +141 -0
- registry/pegasus/schema.json +76 -0
- registry/thy/__init__.py +5 -0
- registry/thy/client.py +1796 -0
- registry/thy/schema.json +46 -0
- registry/trendyol/__init__.py +5 -0
- registry/trendyol/client.py +228 -0
- registry/trendyol/schema.json +30 -0
- registry/ucak_bileti/client.py +94 -0
- registry/ucak_bileti/schema.json +47 -0
- registry/wikipedia/__init__.py +5 -0
- registry/wikipedia/client.py +64 -0
- registry/wikipedia/schema.json +31 -0
- registry/yahoo_finance/__init__.py +5 -0
- registry/yahoo_finance/client.py +98 -0
- registry/yahoo_finance/schema.json +22 -0
- server.py +891 -0
- static/index.html +2401 -0
compiler.py
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
|
|
19
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
20
|
+
try:
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
from recorder import CapturedEndpoint, CaptureSession
|
|
27
|
+
|
|
28
|
+
console = Console(stderr=True, legacy_windows=False)
|
|
29
|
+
|
|
30
|
+
# Varsayılan Tarayıcı Parmak İzi (Browser Fingerprint Spoofing)
|
|
31
|
+
DEFAULT_USER_AGENT: str = (
|
|
32
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
33
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# httpx tarafından otomatik yönetilen alt seviye aktarım başlıkları
|
|
37
|
+
EXCLUDED_HEADERS: frozenset[str] = frozenset(
|
|
38
|
+
{
|
|
39
|
+
"accept-encoding",
|
|
40
|
+
"accept-language",
|
|
41
|
+
"content-length",
|
|
42
|
+
"host",
|
|
43
|
+
"connection",
|
|
44
|
+
"keep-alive",
|
|
45
|
+
"transfer-encoding",
|
|
46
|
+
"te",
|
|
47
|
+
"trailer",
|
|
48
|
+
"upgrade",
|
|
49
|
+
"proxy-authorization",
|
|
50
|
+
"proxy-authenticate",
|
|
51
|
+
"cache-control",
|
|
52
|
+
"pragma",
|
|
53
|
+
"priority",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Korunması gereken kritik kimlik doğrulama, oturum ve tarayıcı parmak izi başlıkları
|
|
58
|
+
ALLOWED_HEADER_PREFIXES: tuple[str, ...] = (
|
|
59
|
+
"authorization",
|
|
60
|
+
"cookie",
|
|
61
|
+
"content-type",
|
|
62
|
+
"accept",
|
|
63
|
+
"user-agent",
|
|
64
|
+
"referer",
|
|
65
|
+
"origin",
|
|
66
|
+
"sec-ch-ua",
|
|
67
|
+
"sec-ch-ua-mobile",
|
|
68
|
+
"sec-ch-ua-platform",
|
|
69
|
+
"sec-fetch-dest",
|
|
70
|
+
"sec-fetch-mode",
|
|
71
|
+
"sec-fetch-site",
|
|
72
|
+
"x-algolia-",
|
|
73
|
+
"x-api-key",
|
|
74
|
+
"api-key",
|
|
75
|
+
"x-csrf",
|
|
76
|
+
"x-xsrf",
|
|
77
|
+
"csrf",
|
|
78
|
+
"xsrf",
|
|
79
|
+
"x-auth",
|
|
80
|
+
"x-session",
|
|
81
|
+
"x-token",
|
|
82
|
+
"x-requested-with",
|
|
83
|
+
"x-px",
|
|
84
|
+
"px-",
|
|
85
|
+
"_px",
|
|
86
|
+
"pxcts",
|
|
87
|
+
"x-perimeterx",
|
|
88
|
+
"tafsessionid",
|
|
89
|
+
"taftrackingid",
|
|
90
|
+
"x-current-page-url",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# LLM promptuna dahil edilmeyecek 3. taraf reklam, piksel, telemetri ve analitik domain parçaları
|
|
94
|
+
THIRD_PARTY_IGNORE_KEYWORDS: tuple[str, ...] = (
|
|
95
|
+
"yandex",
|
|
96
|
+
"evergage",
|
|
97
|
+
"useinsider",
|
|
98
|
+
"openai",
|
|
99
|
+
"creativecdn",
|
|
100
|
+
"firebase",
|
|
101
|
+
"google-analytics",
|
|
102
|
+
"googletagmanager",
|
|
103
|
+
"doubleclick",
|
|
104
|
+
"googleadservices",
|
|
105
|
+
"facebook",
|
|
106
|
+
"criteo",
|
|
107
|
+
"hotjar",
|
|
108
|
+
"clarity.ms",
|
|
109
|
+
"sentry",
|
|
110
|
+
"mixpanel",
|
|
111
|
+
"amplitude",
|
|
112
|
+
"datadog",
|
|
113
|
+
"newrelic",
|
|
114
|
+
"adroll",
|
|
115
|
+
"taboola",
|
|
116
|
+
"outbrain",
|
|
117
|
+
"tiktok",
|
|
118
|
+
"snapchat",
|
|
119
|
+
"quantummetric",
|
|
120
|
+
"dynatrace",
|
|
121
|
+
"adform",
|
|
122
|
+
"tapad",
|
|
123
|
+
"yabs",
|
|
124
|
+
"pixel",
|
|
125
|
+
"telemetry",
|
|
126
|
+
"beacon",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def extract_root_domain(url_or_domain: str) -> str:
|
|
131
|
+
"""URL veya domain adından kök domaini (örn: web.flypgs.com -> flypgs.com) döndürür."""
|
|
132
|
+
parsed = urlparse(url_or_domain if "://" in url_or_domain else f"https://{url_or_domain}")
|
|
133
|
+
host = (parsed.hostname or url_or_domain).lower().strip()
|
|
134
|
+
parts = host.split(".")
|
|
135
|
+
if len(parts) >= 3 and parts[-2] in ("com", "org", "gov", "edu", "net", "co"):
|
|
136
|
+
return ".".join(parts[-3:])
|
|
137
|
+
if len(parts) >= 2:
|
|
138
|
+
return ".".join(parts[-2:])
|
|
139
|
+
return host
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DynamicParamSpec(BaseModel):
|
|
143
|
+
"""LLM tarafından tespit edilen dinamik parametre tanımı."""
|
|
144
|
+
|
|
145
|
+
name: str = Field(..., description="Python fonksiyon argümanı adı (snake_case, örn: query, page)")
|
|
146
|
+
original_key: str = Field(..., description="Payload veya query_params içindeki orijinal anahtar adı")
|
|
147
|
+
location: str = Field(..., description="Parametrenin yeri: 'body', 'query' veya 'path'")
|
|
148
|
+
param_type: str = Field(default="str", description="Python tipi: 'str', 'int', 'float', 'bool'")
|
|
149
|
+
default_value: Any = Field(..., description="Yakalanan son istekteki örnek/varsayılan değer")
|
|
150
|
+
description: str = Field(default="", description="Parametrenin kısa açıklaması")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class RegistrySchema(BaseModel):
|
|
154
|
+
"""registry/<servis_adi>/schema.json için kalıcı servis şeması."""
|
|
155
|
+
|
|
156
|
+
service_name: str
|
|
157
|
+
target_url: str
|
|
158
|
+
goal: str
|
|
159
|
+
function_name: str
|
|
160
|
+
explanation: str
|
|
161
|
+
method: str
|
|
162
|
+
endpoint_url: str
|
|
163
|
+
dynamic_params: list[DynamicParamSpec] = Field(default_factory=list)
|
|
164
|
+
created_at: str
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class CompilationResult(BaseModel):
|
|
168
|
+
"""Derleme sonucunda üretilen meta veri ve kaynak kodlar."""
|
|
169
|
+
|
|
170
|
+
service_name: str = "default"
|
|
171
|
+
provider: str
|
|
172
|
+
model: str
|
|
173
|
+
matched_endpoint_id: int
|
|
174
|
+
matched_method: str
|
|
175
|
+
matched_url: str
|
|
176
|
+
function_name: str
|
|
177
|
+
explanation: str
|
|
178
|
+
dynamic_params: list[DynamicParamSpec] = Field(default_factory=list)
|
|
179
|
+
client_code: str
|
|
180
|
+
mcp_code: str
|
|
181
|
+
client_path: Path = Path("generated_client.py")
|
|
182
|
+
mcp_path: Path = Path("generated_mcp.py")
|
|
183
|
+
registry_client_path: Path | None = None
|
|
184
|
+
registry_schema_path: Path | None = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def derive_service_name(url_or_domain: str) -> str:
|
|
188
|
+
"""Hedef URL veya domain adından temiz bir servis adı türetir (örn: https://hn.algolia.com -> hn_algolia)."""
|
|
189
|
+
parsed = urlparse(url_or_domain if "://" in url_or_domain else f"https://{url_or_domain}")
|
|
190
|
+
host = (parsed.hostname or url_or_domain).lower().strip()
|
|
191
|
+
if host.startswith("www."):
|
|
192
|
+
host = host[4:]
|
|
193
|
+
# Bilinen üst seviye uzantıları temizle
|
|
194
|
+
for tld in (".com.tr", ".org.tr", ".gov.tr", ".edu.tr", ".co.uk", ".com", ".net", ".org", ".io", ".dev", ".app", ".ai", ".co", ".me"):
|
|
195
|
+
if host.endswith(tld) and len(host) > len(tld):
|
|
196
|
+
host = host[: -len(tld)]
|
|
197
|
+
break
|
|
198
|
+
sanitized = re.sub(r"[^a-z0-9]+", "_", host).strip("_")
|
|
199
|
+
return sanitized or "shadow_service"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
SYSTEM_PROMPT = """\
|
|
203
|
+
Sen kıdemli bir Sistem Mühendisi ve API Tersine Mühendislik (Reverse Engineering) uzmanısın.
|
|
204
|
+
Görevin, yakalanmış ve tekilleştirilmiş XHR/Fetch ağ trafiği (JSON) içinden kullanıcının belirttiği hedefe (`goal`) karşılık gelen ASIL uç noktayı (endpoint) tespit etmek ve bu uç noktayı doğrudan çağıran iki adet üretim kalitesinde Python dosyası (`generated_client.py` ve `generated_mcp.py`) üretmektir.
|
|
205
|
+
|
|
206
|
+
KESİN KURallar:
|
|
207
|
+
1. Sağlık kontrolü (`isalive`, `ping`, `health`), telemetri veya analitik uç noktalarını DEĞİL, doğrudan kullanıcının hedeflediği işi (örn. arama, listeleme, veri çekme) yapan asıl veri uç noktasını seç.
|
|
208
|
+
2. Başlık (Header) Filtrelemesi ve Browser Fingerprint Spoofing (ZORUNLU):
|
|
209
|
+
- `accept-encoding`, `content-length`, `host`, `connection` gibi aktarım başlıklarını koda ekleme.
|
|
210
|
+
- Buna karşılık tarayıcı parmak izi (fingerprint) ve kimlik başlıklarını (`user-agent`, `referer`, `origin`, `accept`, `sec-ch-ua*`, `sec-fetch-*`, `x-requested-with`, `x-algolia-*`, `Authorization`, `Cookie`, `Content-Type`) MUTLAKA koru.
|
|
211
|
+
- Üretilen `headers` sözlüğünde eğer `user-agent` yoksa varsayılan olarak `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"`, hedef sitenin `referer` başlığı ve `"accept": "application/json"` başlığı ZORUNLU olarak bulunmalıdır.
|
|
212
|
+
- ÖNEMLİ: Eğer yakalanan isteğin `request_payload` alanı bir JSON nesnesi (dict/list) ise ve `content-type` başlığı `application/x-www-form-urlencoded` olarak görünüyorsa, `httpx` çağrısında gövdeyi mutlaka `json=payload` olarak gönder (`data=payload` KULLANMA).
|
|
213
|
+
3. Dinamik Parametre Çıkarımı:
|
|
214
|
+
- Kullanıcının amacına göre (ör. arama sorgusu `query`, sayfa numarası `page`, limit `hitsPerPage` vb.) dinamik olması gereken parametreleri fonksiyon argümanı haline getir.
|
|
215
|
+
- Fonksiyon argümanlarına yakalanan son istekteki değerleri varsayılan (default) değer olarak ata ki fonksiyon argümansız çağrıldığında bile doğrudan çalışabilsin.
|
|
216
|
+
- Statik kalan diğer payload ve query parametrelerini fonksiyon içinde koru.
|
|
217
|
+
- Uçuş/Seyahat Arama ve Gidiş-Dönüş (Round-Trip) Desteği:
|
|
218
|
+
* Kullanıcı gidiş-dönüş sorgusu yaptığında (`return_date` parametresi dolu olduğunda), THY ve havayolu API'lerine gönderilen `originDestinationInformationList` payload şemasına hem gidiş (örn. IST -> ESB) hem de dönüş (örn. ESB -> IST) segmentlerini eksiksiz ekle ve `selectedBookerSearch` değerini 'R' yap.
|
|
219
|
+
* `return_date` parametresi boş veya None olduğunda ise tek yönlü (One-Way) arama olarak tek segmentli yapıyı koru ve `selectedBookerSearch` değerini 'O' yap.
|
|
220
|
+
4. `generated_client.py` Gereksinimleri (TARAYICI İÇİ JAVASCRIPT ÇALIŞTIRMA VE ENTERPRISE WAF/BOT BYPASS ZORUNLU):
|
|
221
|
+
- DIŞARIDAN DOĞRUDAN HTTP veya `curl_cffi` ISTEĞI ATMA. Akamai, Cloudflare ve cpr_chlge korumaları dışarıdan atılan istekleri anında 403 Forbidden veya 406 ile bloklar.
|
|
222
|
+
- Bunun yerine tarayıcı oturumu üzerinden tarayıcının kendi JavaScript ortamında (`page.evaluate("fetch(...)")`) istek çalıştırılmalıdır.
|
|
223
|
+
- Orijinal Gezinme URL'si (Referer):
|
|
224
|
+
* Tarayıcı oturumunu başlatırken ve `headers` içindeki `referer` alanında kök domain yerine doğrudan genel rezervasyon ana giriş sayfasını (örn. `https://www.turkishairlines.com/tr-tr/ucak-bileti/rezervasyon/`) kullan ki timeout hatası oluşmadan sayfa hızlıca açılsın ve PerimeterX "basılı tut" doğrulaması doğrudan ekrana gelsin.
|
|
225
|
+
- Dosyanın en başına `import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))` ve `from healer import SessionHealer, update_payload_deep` içe aktarımını ekle.
|
|
226
|
+
- Temiz, tip güvenli (type-hinted) ve docstring içeren bir ana fonksiyon yaz.
|
|
227
|
+
- Fonksiyon içinde dinamik parametreleri güncelle (`update_payload_deep`), ardından `healer = SessionHealer()` oluştur.
|
|
228
|
+
- İsteği tarayıcı context'i içinde çalıştırmak ve sonucu almak için doğrudan `return healer.execute_in_browser(method=..., url=..., headers=headers, params=params, json_payload=payload)` çağrısını yap.
|
|
229
|
+
- `if __name__ == "__main__":` bloğunda örnek bir sorgu çalıştır, süreyi ölç ve dönen JSON yanıtının anlamlı bir özetini terminale anlaşılır biçimde yazdır.
|
|
230
|
+
5. `generated_mcp.py` Gereksinimleri:
|
|
231
|
+
- `from mcp.server.fastmcp import FastMCP` kullan (`mcp = FastMCP("ShadowAPI")`).
|
|
232
|
+
- Aynı işlevi `@mcp.tool()` dekoratörüyle dışa aç.
|
|
233
|
+
- `if __name__ == "__main__": mcp.run()` bloğunu ekle.
|
|
234
|
+
6. Yanıtını SADECE aşağıdaki JSON şemasında döndür (Markdown kod bloğu olmadan veya tek bir ```json bloğu içinde):
|
|
235
|
+
{
|
|
236
|
+
"matched_endpoint_id": <int>,
|
|
237
|
+
"function_name": "<snake_case_fonksiyon_adi>",
|
|
238
|
+
"explanation": "<Neden bu uç noktanın seçildiği ve hangi parametrelerin dinamikleştirildiğinin kısa Türkçe açıklaması>",
|
|
239
|
+
"dynamic_params": [
|
|
240
|
+
{
|
|
241
|
+
"name": "<arguman_adi>",
|
|
242
|
+
"original_key": "<orijinal_anahtar>",
|
|
243
|
+
"location": "body | query | path",
|
|
244
|
+
"param_type": "str | int | float | bool",
|
|
245
|
+
"default_value": <varsayilan_deger>,
|
|
246
|
+
"description": "<açıklama>"
|
|
247
|
+
}
|
|
248
|
+
],
|
|
249
|
+
"client_code": "<generated_client.py tam Python kodu>",
|
|
250
|
+
"mcp_code": "<generated_mcp.py tam Python kodu>"
|
|
251
|
+
}
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ShadowAPICompiler:
|
|
256
|
+
"""Yakalanan ağ trafiğini LLM yardımıyla Python SDK ve MCP sunucusuna derler."""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
traffic_path: Path = Path("captured_traffic.json"),
|
|
261
|
+
client_output_path: Path = Path("generated_client.py"),
|
|
262
|
+
mcp_output_path: Path = Path("generated_mcp.py"),
|
|
263
|
+
registry_root: Path = Path("registry"),
|
|
264
|
+
) -> None:
|
|
265
|
+
load_dotenv(override=False)
|
|
266
|
+
self.traffic_path = traffic_path
|
|
267
|
+
self.client_output_path = client_output_path
|
|
268
|
+
self.mcp_output_path = mcp_output_path
|
|
269
|
+
self.registry_root = registry_root
|
|
270
|
+
self.provider, self.api_key, self.model = self._detect_llm_provider()
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _detect_llm_provider() -> tuple[str, str, str]:
|
|
274
|
+
"""Ortam değişkenlerinden (.env) uygun LLM sağlayıcısını (Anthropic veya OpenAI) seçer."""
|
|
275
|
+
anthropic_key = os.getenv("ANTHROPIC_API_KEY", "").strip()
|
|
276
|
+
openai_key = os.getenv("OPENAI_API_KEY", "").strip()
|
|
277
|
+
|
|
278
|
+
if anthropic_key:
|
|
279
|
+
model = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-20250514").strip()
|
|
280
|
+
return ("anthropic", anthropic_key, model)
|
|
281
|
+
if openai_key:
|
|
282
|
+
model = os.getenv("OPENAI_MODEL", "gpt-4o").strip()
|
|
283
|
+
return ("openai", openai_key, model)
|
|
284
|
+
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
"LLM API anahtarı bulunamadı! Lütfen `.env` dosyasına veya ortam değişkenlerine "
|
|
287
|
+
"`ANTHROPIC_API_KEY` veya `OPENAI_API_KEY` ekleyin."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def sanitize_headers(headers: dict[str, str], target_url: str = "") -> dict[str, str]:
|
|
292
|
+
"""
|
|
293
|
+
httpx tarafından otomatik yönetilen aktarım başlıklarını temizler ve
|
|
294
|
+
Browser Fingerprint Spoofing (User-Agent, Referer, Accept) başlıklarını zorunlu olarak ekler.
|
|
295
|
+
"""
|
|
296
|
+
cleaned: dict[str, str] = {}
|
|
297
|
+
for k, v in headers.items():
|
|
298
|
+
lower_k = k.lower().strip()
|
|
299
|
+
if lower_k in EXCLUDED_HEADERS:
|
|
300
|
+
continue
|
|
301
|
+
if any(lower_k == prefix or lower_k.startswith(prefix) for prefix in ALLOWED_HEADER_PREFIXES):
|
|
302
|
+
cleaned[lower_k] = v
|
|
303
|
+
|
|
304
|
+
# Browser Fingerprint Spoofing zorunlu varsayılanlar
|
|
305
|
+
if "user-agent" not in cleaned:
|
|
306
|
+
cleaned["user-agent"] = DEFAULT_USER_AGENT
|
|
307
|
+
if "referer" not in cleaned and target_url:
|
|
308
|
+
cleaned["referer"] = target_url if target_url.endswith("/") else f"{target_url}/"
|
|
309
|
+
if "accept" not in cleaned:
|
|
310
|
+
cleaned["accept"] = "application/json"
|
|
311
|
+
|
|
312
|
+
return cleaned
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def filter_relevant_endpoints(
|
|
316
|
+
cls,
|
|
317
|
+
endpoints: list[CapturedEndpoint],
|
|
318
|
+
target_url: str = "",
|
|
319
|
+
) -> list[CapturedEndpoint]:
|
|
320
|
+
"""
|
|
321
|
+
LLM promptuna gönderilecek uç noktaları süzer:
|
|
322
|
+
1. 3. taraf reklam, piksel, telemetri ve analitik domainlerini (yandex, evergage, useinsider, openai, creativecdn, firebase vb.) eler.
|
|
323
|
+
2. Eğer hedef URL belirtilmişse hedef root domain ile eşleşen ana veri uç noktalarını korur.
|
|
324
|
+
3. Bot algılama sensör/challenge uç noktalarını (örn: /dRRXeAPrg/, /LSrVcDPgS/) veri API'si olmadığı için eler.
|
|
325
|
+
"""
|
|
326
|
+
target_root = extract_root_domain(target_url) if target_url else ""
|
|
327
|
+
filtered: list[CapturedEndpoint] = []
|
|
328
|
+
for ep in endpoints:
|
|
329
|
+
dom = ep.domain.lower()
|
|
330
|
+
path = ep.path.lower()
|
|
331
|
+
|
|
332
|
+
# 1. 3. taraf reklam, piksel ve analitik filtrelemesi
|
|
333
|
+
if any(kw in dom for kw in THIRD_PARTY_IGNORE_KEYWORDS):
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
# 2. Akamai / WAF sensör ve challenge veri yollarını filtrele
|
|
337
|
+
if any(s in path for s in ("/drrxeaprg/", "/lsrvcdpgs/image", "/_incapsula_resource", "/challenge")):
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# 3. Hedef domain eşleşmesi (sadece hedef servise ait uç noktalar)
|
|
341
|
+
if target_root:
|
|
342
|
+
ep_root = extract_root_domain(dom)
|
|
343
|
+
is_match = (
|
|
344
|
+
ep_root == target_root
|
|
345
|
+
or target_root in dom
|
|
346
|
+
or (target_root in ("algolia.com", "hackernews") and "algolia" in dom)
|
|
347
|
+
)
|
|
348
|
+
if not is_match:
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
filtered.append(ep)
|
|
352
|
+
|
|
353
|
+
return filtered or endpoints
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def deduplicate_endpoints(
|
|
357
|
+
cls,
|
|
358
|
+
endpoints: list[CapturedEndpoint],
|
|
359
|
+
target_url: str = "",
|
|
360
|
+
) -> list[CapturedEndpoint]:
|
|
361
|
+
"""
|
|
362
|
+
Aynı (method, domain, path) kombinasyonuna sahip tekrar eden istekleri tekilleştirir
|
|
363
|
+
ve her uç noktanın EN SON yakalanan örneğini (kullanıcının yazmayı bitirdiği son payload'u) korur.
|
|
364
|
+
"""
|
|
365
|
+
dedup_map: dict[tuple[str, str, str], CapturedEndpoint] = {}
|
|
366
|
+
for ep in endpoints:
|
|
367
|
+
key = (ep.method.upper(), ep.domain.lower(), ep.path)
|
|
368
|
+
# Her yeni eşleşme öncekini ezer; böylece en son yakalanan örnek kalır.
|
|
369
|
+
cleaned_ep = ep.model_copy(deep=True)
|
|
370
|
+
ref_url = target_url or f"https://{ep.domain}"
|
|
371
|
+
cleaned_ep.headers = cls.sanitize_headers(cleaned_ep.headers, target_url=ref_url)
|
|
372
|
+
dedup_map[key] = cleaned_ep
|
|
373
|
+
|
|
374
|
+
return list(dedup_map.values())
|
|
375
|
+
|
|
376
|
+
def load_and_prepare_traffic(self) -> tuple[CaptureSession, list[CapturedEndpoint]]:
|
|
377
|
+
"""captured_traffic.json dosyasını okur, 3. taraf domainleri filtreler ve tekilleştirilmiş uç noktaları döndürür."""
|
|
378
|
+
if not self.traffic_path.exists():
|
|
379
|
+
raise FileNotFoundError(
|
|
380
|
+
f"Trafik dosyası bulunamadı: {self.traffic_path}. Önce kayıt (record/build) yapmalısınız."
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
raw_content = self.traffic_path.read_text(encoding="utf-8")
|
|
384
|
+
session = CaptureSession.model_validate_json(raw_content)
|
|
385
|
+
|
|
386
|
+
if not session.endpoints:
|
|
387
|
+
raise ValueError(f"{self.traffic_path} içinde yakalanmış hiçbir uç nokta bulunmuyor.")
|
|
388
|
+
|
|
389
|
+
# 1. 3. taraf reklam, piksel, telemetri ve analitikleri filtrele (drop et)
|
|
390
|
+
relevant_endpoints = self.filter_relevant_endpoints(session.endpoints, target_url=session.target_url)
|
|
391
|
+
|
|
392
|
+
# 2. Tekilleştir ve son istek halini koru
|
|
393
|
+
deduped = self.deduplicate_endpoints(relevant_endpoints, target_url=session.target_url)
|
|
394
|
+
return session, deduped
|
|
395
|
+
|
|
396
|
+
def _build_user_prompt(self, goal: str, target_url: str, endpoints: list[CapturedEndpoint]) -> str:
|
|
397
|
+
"""LLM'e gönderilecek kullanıcı istemini hazırlar."""
|
|
398
|
+
serialized_endpoints = [
|
|
399
|
+
{
|
|
400
|
+
"id": ep.id,
|
|
401
|
+
"method": ep.method,
|
|
402
|
+
"url": ep.url,
|
|
403
|
+
"domain": ep.domain,
|
|
404
|
+
"path": ep.path,
|
|
405
|
+
"status_code": ep.status_code,
|
|
406
|
+
"query_params": ep.query_params,
|
|
407
|
+
"headers": ep.headers,
|
|
408
|
+
"request_payload": ep.request_payload,
|
|
409
|
+
"response_sample": ep.response_sample[:1200],
|
|
410
|
+
}
|
|
411
|
+
for ep in endpoints
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
f"Hedef Web Sitesi: {target_url}\n"
|
|
416
|
+
f"Kullanıcının Hedefi (Goal): {goal}\n\n"
|
|
417
|
+
f"Tekilleştirilmiş Uç Nokta Listesi (Her uç noktanın EN SON yakalanan hali):\n"
|
|
418
|
+
f"{json.dumps(serialized_endpoints, ensure_ascii=False, indent=2)}\n\n"
|
|
419
|
+
"Lütfen hedefe en uygun uç noktayı seç ve `generated_client.py` ile `generated_mcp.py` "
|
|
420
|
+
"dosyalarını istenen JSON formatında üret."
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _call_anthropic(self, user_prompt: str) -> str:
|
|
424
|
+
"""Anthropic Claude API çağrısını gerçekleştirir (model yedeklemeli)."""
|
|
425
|
+
from anthropic import Anthropic
|
|
426
|
+
|
|
427
|
+
client = Anthropic(api_key=self.api_key)
|
|
428
|
+
candidate_models = [
|
|
429
|
+
self.model,
|
|
430
|
+
"claude-sonnet-4-20250514",
|
|
431
|
+
"claude-3-7-sonnet-latest",
|
|
432
|
+
"claude-3-5-sonnet-latest",
|
|
433
|
+
"claude-3-5-haiku-latest",
|
|
434
|
+
]
|
|
435
|
+
# Sırayı koruyarak tekilleştir
|
|
436
|
+
models_to_try = list(dict.fromkeys(candidate_models))
|
|
437
|
+
|
|
438
|
+
last_err: Exception | None = None
|
|
439
|
+
for model_name in models_to_try:
|
|
440
|
+
try:
|
|
441
|
+
response = client.messages.create(
|
|
442
|
+
model=model_name,
|
|
443
|
+
max_tokens=4096,
|
|
444
|
+
temperature=0.1,
|
|
445
|
+
system=SYSTEM_PROMPT,
|
|
446
|
+
messages=[{"role": "user", "content": user_prompt}],
|
|
447
|
+
)
|
|
448
|
+
self.model = model_name
|
|
449
|
+
text_blocks = [block.text for block in response.content if hasattr(block, "text")]
|
|
450
|
+
return "\n".join(text_blocks)
|
|
451
|
+
except Exception as exc:
|
|
452
|
+
last_err = exc
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
raise RuntimeError(f"Anthropic API çağrısı başarısız oldu: {last_err}")
|
|
456
|
+
|
|
457
|
+
def _call_openai(self, user_prompt: str) -> str:
|
|
458
|
+
"""OpenAI API çağrısını gerçekleştirir."""
|
|
459
|
+
from openai import OpenAI
|
|
460
|
+
|
|
461
|
+
base_url = os.getenv("OPENAI_BASE_URL")
|
|
462
|
+
client = OpenAI(api_key=self.api_key, base_url=base_url) if base_url else OpenAI(api_key=self.api_key)
|
|
463
|
+
|
|
464
|
+
response = client.chat.completions.create(
|
|
465
|
+
model=self.model,
|
|
466
|
+
temperature=0.1,
|
|
467
|
+
response_format={"type": "json_object"},
|
|
468
|
+
messages=[
|
|
469
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
470
|
+
{"role": "user", "content": user_prompt},
|
|
471
|
+
],
|
|
472
|
+
)
|
|
473
|
+
return response.choices[0].message.content or "{}"
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def _extract_json_dict(raw_text: str) -> dict[str, Any]:
|
|
477
|
+
"""LLM yanıtından JSON nesnesini güvenli biçimde ayıklar."""
|
|
478
|
+
cleaned = raw_text.strip()
|
|
479
|
+
# Eğer ```json ... ``` bloğu içindeyse ayıkla
|
|
480
|
+
fence_match = re.search(r"```(?:json)?\s*(\{.*\})\s*```", cleaned, flags=re.DOTALL)
|
|
481
|
+
if fence_match:
|
|
482
|
+
cleaned = fence_match.group(1).strip()
|
|
483
|
+
else:
|
|
484
|
+
start = cleaned.find("{")
|
|
485
|
+
end = cleaned.rfind("}")
|
|
486
|
+
if start != -1 and end != -1 and end > start:
|
|
487
|
+
cleaned = cleaned[start : end + 1]
|
|
488
|
+
|
|
489
|
+
return json.loads(cleaned)
|
|
490
|
+
|
|
491
|
+
@staticmethod
|
|
492
|
+
def _validate_python_syntax(code: str, filename: str) -> bool:
|
|
493
|
+
"""Üretilen Python kodunun AST sözdizimi doğrulamasını yapar."""
|
|
494
|
+
try:
|
|
495
|
+
ast.parse(code, filename=filename)
|
|
496
|
+
return True
|
|
497
|
+
except SyntaxError:
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
@staticmethod
|
|
501
|
+
def _build_fallback_codes(
|
|
502
|
+
endpoint: CapturedEndpoint,
|
|
503
|
+
function_name: str,
|
|
504
|
+
dynamic_params: list[DynamicParamSpec],
|
|
505
|
+
explanation: str,
|
|
506
|
+
) -> tuple[str, str]:
|
|
507
|
+
"""LLM'in ürettiği kodda sözdizimi hatası olursa devreye giren deterministik kod üretici."""
|
|
508
|
+
fn_name = re.sub(r"[^a-zA-Z0-9_]", "_", function_name or "call_shadow_api").lower()
|
|
509
|
+
if not fn_name or fn_name[0].isdigit():
|
|
510
|
+
fn_name = f"api_{fn_name}"
|
|
511
|
+
|
|
512
|
+
# Eğer LLM dinamik parametre döndürmediyse, payload veya query içinden otomatik tahmin et
|
|
513
|
+
if not dynamic_params:
|
|
514
|
+
if isinstance(endpoint.request_payload, dict) and "query" in endpoint.request_payload:
|
|
515
|
+
dynamic_params = [
|
|
516
|
+
DynamicParamSpec(
|
|
517
|
+
name="query",
|
|
518
|
+
original_key="query",
|
|
519
|
+
location="body",
|
|
520
|
+
param_type="str",
|
|
521
|
+
default_value=endpoint.request_payload.get("query", ""),
|
|
522
|
+
description="Arama sorgusu",
|
|
523
|
+
)
|
|
524
|
+
]
|
|
525
|
+
elif "query" in endpoint.query_params or "q" in endpoint.query_params:
|
|
526
|
+
k = "query" if "query" in endpoint.query_params else "q"
|
|
527
|
+
dynamic_params = [
|
|
528
|
+
DynamicParamSpec(
|
|
529
|
+
name=k,
|
|
530
|
+
original_key=k,
|
|
531
|
+
location="query",
|
|
532
|
+
param_type="str",
|
|
533
|
+
default_value=endpoint.query_params[k],
|
|
534
|
+
description="Arama sorgusu",
|
|
535
|
+
)
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
# Fonksiyon imzası oluştur
|
|
539
|
+
sig_parts: list[str] = []
|
|
540
|
+
for p in dynamic_params:
|
|
541
|
+
py_type = p.param_type if p.param_type in ("str", "int", "float", "bool") else "Any"
|
|
542
|
+
sig_parts.append(f"{p.name}: {py_type} = {repr(p.default_value)}")
|
|
543
|
+
sig_str = ", ".join(sig_parts)
|
|
544
|
+
|
|
545
|
+
# Eğer payload JSON ise ve content-type form-urlencoded ise httpx'in json= göndermesi için ayarla
|
|
546
|
+
headers_dict = ShadowAPICompiler.sanitize_headers(
|
|
547
|
+
endpoint.headers,
|
|
548
|
+
target_url=f"https://{endpoint.domain}",
|
|
549
|
+
)
|
|
550
|
+
if headers_dict.get("sec-fetch-dest") == "document":
|
|
551
|
+
headers_dict["sec-fetch-dest"] = "empty"
|
|
552
|
+
if headers_dict.get("sec-fetch-mode") == "navigate":
|
|
553
|
+
headers_dict["sec-fetch-mode"] = "cors"
|
|
554
|
+
|
|
555
|
+
cleaned_payload = copy.deepcopy(endpoint.request_payload)
|
|
556
|
+
if isinstance(cleaned_payload, dict):
|
|
557
|
+
for k in list(cleaned_payload.keys()):
|
|
558
|
+
v = cleaned_payload[k]
|
|
559
|
+
if v is None:
|
|
560
|
+
cleaned_payload.pop(k, None)
|
|
561
|
+
elif isinstance(v, dict) and all(sub_v is None for sub_v in v.values()):
|
|
562
|
+
cleaned_payload.pop(k, None)
|
|
563
|
+
elif k.lower() in ("operationcode", "personnelflightsearch", "expatpnr", "ffredemption") and v in (False, None, "TK"):
|
|
564
|
+
cleaned_payload.pop(k, None)
|
|
565
|
+
|
|
566
|
+
if isinstance(cleaned_payload, (dict, list)):
|
|
567
|
+
if headers_dict.get("content-type", "").lower() == "application/x-www-form-urlencoded":
|
|
568
|
+
headers_dict.pop("content-type", None)
|
|
569
|
+
|
|
570
|
+
body_override_pairs: list[str] = []
|
|
571
|
+
query_override_lines: list[str] = []
|
|
572
|
+
for p in dynamic_params:
|
|
573
|
+
if p.location == "body" and isinstance(cleaned_payload, (dict, list)):
|
|
574
|
+
body_override_pairs.append(f"{repr(p.original_key)}: {p.name}")
|
|
575
|
+
elif p.location == "query":
|
|
576
|
+
query_override_lines.append(f" params[{repr(p.original_key)}] = {p.name}")
|
|
577
|
+
|
|
578
|
+
is_flight_search = isinstance(cleaned_payload, dict) and "originDestinationInformationList" in cleaned_payload
|
|
579
|
+
if is_flight_search:
|
|
580
|
+
body_updates = f""" is_round_trip = bool(locals().get('return_date') and str(locals().get('return_date')).strip())
|
|
581
|
+
if isinstance(payload, dict) and "originDestinationInformationList" in payload:
|
|
582
|
+
od_list = payload["originDestinationInformationList"]
|
|
583
|
+
if is_round_trip:
|
|
584
|
+
payload["selectedBookerSearch"] = "R"
|
|
585
|
+
if len(od_list) == 1:
|
|
586
|
+
import copy
|
|
587
|
+
outbound = od_list[0]
|
|
588
|
+
inbound = copy.deepcopy(outbound)
|
|
589
|
+
inbound["originAirportCode"] = outbound.get("destinationAirportCode")
|
|
590
|
+
inbound["destinationAirportCode"] = outbound.get("originAirportCode")
|
|
591
|
+
if "departureDate" in inbound and locals().get("return_date"):
|
|
592
|
+
inbound["departureDate"] = locals().get("return_date")
|
|
593
|
+
payload["originDestinationInformationList"] = [outbound, inbound]
|
|
594
|
+
else:
|
|
595
|
+
payload["selectedBookerSearch"] = "O"
|
|
596
|
+
if len(od_list) > 1:
|
|
597
|
+
payload["originDestinationInformationList"] = [od_list[0]]
|
|
598
|
+
if {repr(bool(body_override_pairs))}:
|
|
599
|
+
payload = update_payload_deep(payload, {{{', '.join(body_override_pairs)}}})"""
|
|
600
|
+
else:
|
|
601
|
+
body_updates = (
|
|
602
|
+
f" payload = update_payload_deep(payload, {{{', '.join(body_override_pairs)}}})"
|
|
603
|
+
if body_override_pairs
|
|
604
|
+
else " # Payload parametresi güncellemesi yok"
|
|
605
|
+
)
|
|
606
|
+
query_updates = "\n".join(query_override_lines)
|
|
607
|
+
|
|
608
|
+
has_json_body = isinstance(cleaned_payload, (dict, list))
|
|
609
|
+
has_raw_body = cleaned_payload is not None and not has_json_body
|
|
610
|
+
|
|
611
|
+
request_call_args = [
|
|
612
|
+
f"method={repr(endpoint.method)}",
|
|
613
|
+
f"url={repr(endpoint.url)}",
|
|
614
|
+
"params=params",
|
|
615
|
+
"headers=headers",
|
|
616
|
+
]
|
|
617
|
+
if has_json_body:
|
|
618
|
+
request_call_args.append("json=payload")
|
|
619
|
+
elif has_raw_body:
|
|
620
|
+
request_call_args.append("content=payload")
|
|
621
|
+
|
|
622
|
+
call_args_str = ", ".join(request_call_args)
|
|
623
|
+
|
|
624
|
+
client_code = f'''from __future__ import annotations
|
|
625
|
+
|
|
626
|
+
import json
|
|
627
|
+
import pathlib
|
|
628
|
+
import sys
|
|
629
|
+
import time
|
|
630
|
+
from typing import Any
|
|
631
|
+
|
|
632
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
|
|
633
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
634
|
+
from healer import SessionHealer, update_payload_deep
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def {fn_name}({sig_str}) -> dict[str, Any]:
|
|
638
|
+
"""
|
|
639
|
+
{explanation}
|
|
640
|
+
Hedef Endpoint: {endpoint.method} {endpoint.url}
|
|
641
|
+
"""
|
|
642
|
+
params: dict[str, Any] = {repr(endpoint.query_params)}
|
|
643
|
+
headers: dict[str, str] = {repr(headers_dict)}
|
|
644
|
+
payload: Any = {repr(cleaned_payload)}
|
|
645
|
+
{query_updates if query_updates else " # Query parametresi güncellemesi yok"}
|
|
646
|
+
{body_updates}
|
|
647
|
+
|
|
648
|
+
healer = SessionHealer()
|
|
649
|
+
# Dışarıdan doğrudan HTTP atmak yerine tarayıcı oturumu üzerinden
|
|
650
|
+
# tarayıcının kendi JavaScript ortamında isteği çalıştır (WAF / Akamai bypass)
|
|
651
|
+
return healer.execute_in_browser(
|
|
652
|
+
method={repr(endpoint.method)},
|
|
653
|
+
url={repr(endpoint.url)},
|
|
654
|
+
headers=headers,
|
|
655
|
+
params=params,
|
|
656
|
+
json_payload=payload if {repr(has_json_body)} else None,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
if __name__ == "__main__":
|
|
661
|
+
start_ns = time.perf_counter()
|
|
662
|
+
data = {fn_name}()
|
|
663
|
+
elapsed_ms = (time.perf_counter() - start_ns) * 1000
|
|
664
|
+
|
|
665
|
+
print(f"[Shadow-API] Yanıt Süresi: {{elapsed_ms:.2f}} ms")
|
|
666
|
+
if isinstance(data, dict):
|
|
667
|
+
if "hits" in data and isinstance(data["hits"], list):
|
|
668
|
+
hits = data["hits"]
|
|
669
|
+
print(f"[Shadow-API] Toplam Sonuç (nbHits): {{data.get('nbHits', len(hits))}} | Dönen: {{len(hits)}}")
|
|
670
|
+
for idx, item in enumerate(hits[:5], start=1):
|
|
671
|
+
title = item.get("title") or item.get("story_title") or item.get("name") or str(item)[:80]
|
|
672
|
+
url = item.get("url") or item.get("story_url") or ""
|
|
673
|
+
print(f" {{idx}}. {{title}} {{f'({{url}})' if url else ''}}")
|
|
674
|
+
elif "pages" in data and isinstance(data["pages"], list):
|
|
675
|
+
pages = data["pages"]
|
|
676
|
+
print(f"[Shadow-API] Bulunan Sayfa Sayısı: {{len(pages)}}")
|
|
677
|
+
for idx, item in enumerate(pages[:5], start=1):
|
|
678
|
+
title = item.get("title") or item.get("key") or str(item)[:80]
|
|
679
|
+
desc = item.get("description") or ""
|
|
680
|
+
key = item.get("key") or ""
|
|
681
|
+
wiki_url = f"https://en.wikipedia.org/wiki/{{key}}" if key else ""
|
|
682
|
+
print(f" {{idx}}. {{title}} — {{desc}} {{f'({{wiki_url}})' if wiki_url else ''}}")
|
|
683
|
+
else:
|
|
684
|
+
print(f"[Shadow-API] Üst Düzey Anahtarlar: {{list(data.keys())}}")
|
|
685
|
+
preview = json.dumps(data, ensure_ascii=False, indent=2)[:600]
|
|
686
|
+
print(preview)
|
|
687
|
+
else:
|
|
688
|
+
print(str(data)[:600])
|
|
689
|
+
'''
|
|
690
|
+
|
|
691
|
+
mcp_code = f'''from __future__ import annotations
|
|
692
|
+
|
|
693
|
+
from typing import Any
|
|
694
|
+
|
|
695
|
+
from mcp.server.fastmcp import FastMCP
|
|
696
|
+
|
|
697
|
+
from generated_client import {fn_name} as _shadow_client_fn
|
|
698
|
+
|
|
699
|
+
mcp = FastMCP("Shadow-API MCP Server")
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@mcp.tool()
|
|
703
|
+
def {fn_name}({sig_str}) -> dict[str, Any]:
|
|
704
|
+
"""
|
|
705
|
+
{explanation}
|
|
706
|
+
"""
|
|
707
|
+
return _shadow_client_fn({", ".join(f"{p.name}={p.name}" for p in dynamic_params)})
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
if __name__ == "__main__":
|
|
711
|
+
mcp.run()
|
|
712
|
+
'''
|
|
713
|
+
return client_code, mcp_code
|
|
714
|
+
|
|
715
|
+
def _save_to_registry(
|
|
716
|
+
self,
|
|
717
|
+
service_name: str,
|
|
718
|
+
target_url: str,
|
|
719
|
+
goal: str,
|
|
720
|
+
function_name: str,
|
|
721
|
+
explanation: str,
|
|
722
|
+
method: str,
|
|
723
|
+
endpoint_url: str,
|
|
724
|
+
dynamic_params: list[DynamicParamSpec],
|
|
725
|
+
client_code: str,
|
|
726
|
+
) -> tuple[Path, Path]:
|
|
727
|
+
"""Derlenen istemciyi ve meta veri şemasını registry/<servis_adi>/ altına kaydeder."""
|
|
728
|
+
service_dir = self.registry_root / service_name
|
|
729
|
+
service_dir.mkdir(parents=True, exist_ok=True)
|
|
730
|
+
|
|
731
|
+
reg_client_path = service_dir / "client.py"
|
|
732
|
+
reg_schema_path = service_dir / "schema.json"
|
|
733
|
+
|
|
734
|
+
reg_client_path.write_text(client_code + "\n", encoding="utf-8")
|
|
735
|
+
|
|
736
|
+
schema_obj = RegistrySchema(
|
|
737
|
+
service_name=service_name,
|
|
738
|
+
target_url=target_url,
|
|
739
|
+
goal=goal,
|
|
740
|
+
function_name=function_name,
|
|
741
|
+
explanation=explanation,
|
|
742
|
+
method=method,
|
|
743
|
+
endpoint_url=endpoint_url,
|
|
744
|
+
dynamic_params=dynamic_params,
|
|
745
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
746
|
+
)
|
|
747
|
+
reg_schema_path.write_text(
|
|
748
|
+
schema_obj.model_dump_json(indent=2) + "\n",
|
|
749
|
+
encoding="utf-8",
|
|
750
|
+
)
|
|
751
|
+
return reg_client_path, reg_schema_path
|
|
752
|
+
|
|
753
|
+
def compile(self, goal: str, service_name: str | None = None) -> CompilationResult:
|
|
754
|
+
"""Trafiği analiz eder, LLM ile derler ve hem kök dosyaları hem de registry kaydını yazar."""
|
|
755
|
+
session, deduped_endpoints = self.load_and_prepare_traffic()
|
|
756
|
+
resolved_service = (
|
|
757
|
+
re.sub(r"[^a-z0-9_]+", "_", service_name.lower().strip()).strip("_")
|
|
758
|
+
if service_name
|
|
759
|
+
else derive_service_name(session.target_url)
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
console.print(
|
|
763
|
+
Panel.fit(
|
|
764
|
+
f"[bold cyan]Servis Adı:[/bold cyan] [bold yellow]{resolved_service}[/bold yellow]\n"
|
|
765
|
+
f"[bold cyan]Sağlayıcı:[/bold cyan] {self.provider.upper()} ([yellow]{self.model}[/yellow])\n"
|
|
766
|
+
f"[bold cyan]Hedef (Goal):[/bold cyan] {goal}\n"
|
|
767
|
+
f"[bold cyan]Filtreleme & Tekilleştirme:[/bold cyan] {len(session.endpoints)} toplam istek ➔ "
|
|
768
|
+
f"[bold green]{len(deduped_endpoints)} ilgili uç nokta (3. taraf reklam/analitikler elendi)[/bold green]",
|
|
769
|
+
title="🧠 Shadow-API Compiler",
|
|
770
|
+
border_style="magenta",
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
user_prompt = self._build_user_prompt(
|
|
775
|
+
goal=goal,
|
|
776
|
+
target_url=session.target_url,
|
|
777
|
+
endpoints=deduped_endpoints,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
with console.status("[bold magenta]LLM uç noktaları analiz ediyor ve SDK/MCP kodlarını derliyor...[/bold magenta]"):
|
|
781
|
+
if self.provider == "anthropic":
|
|
782
|
+
raw_response = self._call_anthropic(user_prompt)
|
|
783
|
+
else:
|
|
784
|
+
raw_response = self._call_openai(user_prompt)
|
|
785
|
+
|
|
786
|
+
parsed = self._extract_json_dict(raw_response)
|
|
787
|
+
|
|
788
|
+
matched_id = int(parsed.get("matched_endpoint_id", deduped_endpoints[-1].id))
|
|
789
|
+
matched_ep = next((ep for ep in deduped_endpoints if ep.id == matched_id), deduped_endpoints[-1])
|
|
790
|
+
|
|
791
|
+
function_name = str(parsed.get("function_name", "call_shadow_api"))
|
|
792
|
+
explanation = str(parsed.get("explanation", "Hedef uç nokta başarıyla eşleştirildi."))
|
|
793
|
+
raw_params = parsed.get("dynamic_params", [])
|
|
794
|
+
dynamic_params = [DynamicParamSpec.model_validate(p) for p in raw_params if isinstance(p, dict)]
|
|
795
|
+
|
|
796
|
+
client_code = str(parsed.get("client_code", "")).strip()
|
|
797
|
+
mcp_code = str(parsed.get("mcp_code", "")).strip()
|
|
798
|
+
|
|
799
|
+
# Üretilen kodların geçerliliğini, Self-Healing tünelini ve Browser Fingerprint Spoofing entegrasyonunu denetle
|
|
800
|
+
lower_client = client_code.lower()
|
|
801
|
+
if (
|
|
802
|
+
not client_code
|
|
803
|
+
or not mcp_code
|
|
804
|
+
or "SessionHealer" not in client_code
|
|
805
|
+
or "retry_with_fresh_session" not in client_code
|
|
806
|
+
or "user-agent" not in lower_client
|
|
807
|
+
or "referer" not in lower_client
|
|
808
|
+
or not self._validate_python_syntax(client_code, "generated_client.py")
|
|
809
|
+
or not self._validate_python_syntax(mcp_code, "generated_mcp.py")
|
|
810
|
+
):
|
|
811
|
+
console.print(
|
|
812
|
+
"[yellow]⚠ Deterministik Self-Healing & Taze Oturum Tüneli şablonu ile istemci kodu üretiliyor...[/yellow]"
|
|
813
|
+
)
|
|
814
|
+
client_code, mcp_code = self._build_fallback_codes(
|
|
815
|
+
endpoint=matched_ep,
|
|
816
|
+
function_name=function_name,
|
|
817
|
+
dynamic_params=dynamic_params,
|
|
818
|
+
explanation=explanation,
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
if "sys.path.insert" not in client_code and "from healer import" in client_code:
|
|
822
|
+
client_code = client_code.replace(
|
|
823
|
+
"from healer import",
|
|
824
|
+
"import sys, pathlib; sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))\nfrom healer import",
|
|
825
|
+
1,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Eşleşen uç noktanın güncel başlık ve tokenlarını session_tokens.json'a yaz
|
|
829
|
+
token_bundle = {
|
|
830
|
+
"target_url": matched_ep.headers.get("referer") or session.target_url,
|
|
831
|
+
"healed_at": datetime.now(timezone.utc).isoformat(),
|
|
832
|
+
"headers": matched_ep.headers,
|
|
833
|
+
"query_params": matched_ep.query_params,
|
|
834
|
+
"body_tokens": {},
|
|
835
|
+
**matched_ep.headers,
|
|
836
|
+
}
|
|
837
|
+
Path("session_tokens.json").write_text(
|
|
838
|
+
json.dumps(token_bundle, ensure_ascii=False, indent=2),
|
|
839
|
+
encoding="utf-8",
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
self.client_output_path.write_text(client_code + "\n", encoding="utf-8")
|
|
843
|
+
self.mcp_output_path.write_text(mcp_code + "\n", encoding="utf-8")
|
|
844
|
+
|
|
845
|
+
reg_client_path, reg_schema_path = self._save_to_registry(
|
|
846
|
+
service_name=resolved_service,
|
|
847
|
+
target_url=session.target_url,
|
|
848
|
+
goal=goal,
|
|
849
|
+
function_name=function_name,
|
|
850
|
+
explanation=explanation,
|
|
851
|
+
method=matched_ep.method,
|
|
852
|
+
endpoint_url=matched_ep.url,
|
|
853
|
+
dynamic_params=dynamic_params,
|
|
854
|
+
client_code=client_code,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
return CompilationResult(
|
|
858
|
+
service_name=resolved_service,
|
|
859
|
+
provider=self.provider,
|
|
860
|
+
model=self.model,
|
|
861
|
+
matched_endpoint_id=matched_ep.id,
|
|
862
|
+
matched_method=matched_ep.method,
|
|
863
|
+
matched_url=matched_ep.url,
|
|
864
|
+
function_name=function_name,
|
|
865
|
+
explanation=explanation,
|
|
866
|
+
dynamic_params=dynamic_params,
|
|
867
|
+
client_code=client_code,
|
|
868
|
+
mcp_code=mcp_code,
|
|
869
|
+
client_path=self.client_output_path,
|
|
870
|
+
mcp_path=self.mcp_output_path,
|
|
871
|
+
registry_client_path=reg_client_path,
|
|
872
|
+
registry_schema_path=reg_schema_path,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
def apply_fallback(self, result: CompilationResult) -> CompilationResult:
|
|
876
|
+
"""Çalışma zamanı hatası durumunda deterministik şablonla dosyaları yeniden üretir."""
|
|
877
|
+
_, deduped_endpoints = self.load_and_prepare_traffic()
|
|
878
|
+
matched_ep = next(
|
|
879
|
+
(ep for ep in deduped_endpoints if ep.id == result.matched_endpoint_id),
|
|
880
|
+
deduped_endpoints[-1],
|
|
881
|
+
)
|
|
882
|
+
client_code, mcp_code = self._build_fallback_codes(
|
|
883
|
+
endpoint=matched_ep,
|
|
884
|
+
function_name=result.function_name,
|
|
885
|
+
dynamic_params=result.dynamic_params,
|
|
886
|
+
explanation=result.explanation,
|
|
887
|
+
)
|
|
888
|
+
self.client_output_path.write_text(client_code + "\n", encoding="utf-8")
|
|
889
|
+
self.mcp_output_path.write_text(mcp_code + "\n", encoding="utf-8")
|
|
890
|
+
if result.registry_client_path:
|
|
891
|
+
result.registry_client_path.write_text(client_code + "\n", encoding="utf-8")
|
|
892
|
+
result.client_code = client_code
|
|
893
|
+
result.mcp_code = mcp_code
|
|
894
|
+
return result
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def ensure_default_registry(registry_root: Path = Path("registry")) -> None:
|
|
898
|
+
"""
|
|
899
|
+
Başlangıçta mevcut generated_client.py varsa ve registry/hackernews/ henüz yoksa
|
|
900
|
+
otomatik olarak registry/hackernews/ altına kaydeder.
|
|
901
|
+
"""
|
|
902
|
+
hn_dir = registry_root / "hackernews"
|
|
903
|
+
hn_client = hn_dir / "client.py"
|
|
904
|
+
hn_schema = hn_dir / "schema.json"
|
|
905
|
+
gen_client = Path("generated_client.py")
|
|
906
|
+
|
|
907
|
+
if gen_client.exists() and (not hn_client.exists() or not hn_schema.exists()):
|
|
908
|
+
hn_dir.mkdir(parents=True, exist_ok=True)
|
|
909
|
+
hn_client.write_text(gen_client.read_text(encoding="utf-8"), encoding="utf-8")
|
|
910
|
+
default_schema = RegistrySchema(
|
|
911
|
+
service_name="hackernews",
|
|
912
|
+
target_url="https://hn.algolia.com",
|
|
913
|
+
goal="Hacker News üzerinde makale araması yap",
|
|
914
|
+
function_name="search_hacker_news_articles",
|
|
915
|
+
explanation="Hacker News (Algolia) arama API uç noktası. Sorgu kelimesine göre makaleleri, puanlarını ve bağlantıları döndürür.",
|
|
916
|
+
method="POST",
|
|
917
|
+
endpoint_url="https://uj5wyc0l7x-dsn.algolia.net/1/indexes/Item_dev/query",
|
|
918
|
+
dynamic_params=[
|
|
919
|
+
DynamicParamSpec(
|
|
920
|
+
name="query",
|
|
921
|
+
original_key="query",
|
|
922
|
+
location="body",
|
|
923
|
+
param_type="str",
|
|
924
|
+
default_value="startup",
|
|
925
|
+
description="Aranacak makale başlığı, anahtar kelime veya konu",
|
|
926
|
+
),
|
|
927
|
+
DynamicParamSpec(
|
|
928
|
+
name="page",
|
|
929
|
+
original_key="page",
|
|
930
|
+
location="body",
|
|
931
|
+
param_type="int",
|
|
932
|
+
default_value=0,
|
|
933
|
+
description="Getirilecek sayfa numarası (0'dan başlar)",
|
|
934
|
+
),
|
|
935
|
+
DynamicParamSpec(
|
|
936
|
+
name="hits_per_page",
|
|
937
|
+
original_key="hitsPerPage",
|
|
938
|
+
location="body",
|
|
939
|
+
param_type="int",
|
|
940
|
+
default_value=30,
|
|
941
|
+
description="Sayfa başına döndürülecek maksimum makale sayısı",
|
|
942
|
+
),
|
|
943
|
+
],
|
|
944
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
945
|
+
)
|
|
946
|
+
hn_schema.write_text(default_schema.model_dump_json(indent=2) + "\n", encoding="utf-8")
|
|
947
|
+
|
|
948
|
+
|