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
core/auto_repair.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import parse_qs, urlparse
|
|
12
|
+
|
|
13
|
+
from playwright.async_api import Response, async_playwright
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
21
|
+
|
|
22
|
+
from compiler import RegistrySchema
|
|
23
|
+
from core.onboarding import (
|
|
24
|
+
MAX_LLM_FIELD_CHARS,
|
|
25
|
+
_compile_with_llm,
|
|
26
|
+
_is_static_asset_url,
|
|
27
|
+
_is_valid_json_response,
|
|
28
|
+
_load_and_call_client,
|
|
29
|
+
_sanitize_endpoint_for_llm,
|
|
30
|
+
_select_meaningful_endpoints,
|
|
31
|
+
_truncate_for_llm,
|
|
32
|
+
validate_and_repair_client,
|
|
33
|
+
)
|
|
34
|
+
from core.token_store import TokenStore
|
|
35
|
+
from recorder import IGNORED_DOMAIN_KEYWORDS, RELEVANT_HEADER_PREFIXES
|
|
36
|
+
|
|
37
|
+
console = Console(stderr=True, legacy_windows=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AutoRepairEngine:
|
|
41
|
+
"""
|
|
42
|
+
Self-Healing 2.0: Otomatik Gece Tamiri ve Zamanlayıcı Motoru.
|
|
43
|
+
- `validate_all_services(runs=1, auto_heal=True)`: `registry/` altındaki tüm aktif servisleri
|
|
44
|
+
sırayla test eder.
|
|
45
|
+
- Testten geçemeyen (`BAŞARISIZ`) servisler için otomatik olarak headless/stealth tarayıcıyla
|
|
46
|
+
hedef URL'ye gidip güncel ağ trafiğini yeniden yakalar ve GPT-4o (`validate_and_repair_client`)
|
|
47
|
+
ile `client.py` dosyasını kendi kendine onarıp 3x doğrulamadan geçirir.
|
|
48
|
+
- Tüm tamir ve sağlık geçmişini `.sessions/auto_repair_report.json` dosyasına kaydeder.
|
|
49
|
+
- `run_daemon(schedule_time="03:00", interval_minutes=None)` ile her gece 03:00'te veya
|
|
50
|
+
belirtilen periyotta otomatik bakım yapar.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
registry_root: Path | str = PROJECT_ROOT / "registry",
|
|
56
|
+
sessions_root: Path | str = PROJECT_ROOT / ".sessions",
|
|
57
|
+
tenant_id: str = "default",
|
|
58
|
+
mode: str = "local",
|
|
59
|
+
) -> None:
|
|
60
|
+
self.registry_root = Path(registry_root)
|
|
61
|
+
self.sessions_root = Path(sessions_root)
|
|
62
|
+
self.sessions_root.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
self.report_path = self.sessions_root / "auto_repair_report.json"
|
|
64
|
+
self.tenant_id = tenant_id or "default"
|
|
65
|
+
self.mode = mode or "local"
|
|
66
|
+
self.token_store = TokenStore(sessions_dir=self.sessions_root)
|
|
67
|
+
|
|
68
|
+
async def _auto_capture_fresh_traffic_async(
|
|
69
|
+
self,
|
|
70
|
+
target_url: str,
|
|
71
|
+
output_path: Path,
|
|
72
|
+
service_name: str,
|
|
73
|
+
wait_seconds: int = 6,
|
|
74
|
+
) -> list[dict[str, Any]]:
|
|
75
|
+
"""
|
|
76
|
+
Bozulan bir servisin hedef URL'sine kullanıcı müdahalesi olmadan (otomatik headless/stealth)
|
|
77
|
+
giderek güncel XHR/Fetch ağ trafiğini ve taze çerezleri yakalar.
|
|
78
|
+
"""
|
|
79
|
+
if service_name.lower() == "thy" or "turkishairlines.com" in (target_url or "").lower():
|
|
80
|
+
target_url = "https://www.turkishairlines.com/tr-tr/"
|
|
81
|
+
else:
|
|
82
|
+
parsed_t = urlparse(target_url or "")
|
|
83
|
+
if "/api/" in parsed_t.path.lower() and parsed_t.scheme and parsed_t.netloc:
|
|
84
|
+
target_url = f"{parsed_t.scheme}://{parsed_t.netloc}/"
|
|
85
|
+
|
|
86
|
+
profile_dir = PROJECT_ROOT / ".browser_profile"
|
|
87
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
captured_endpoints: list[dict[str, Any]] = []
|
|
89
|
+
endpoint_counter = 0
|
|
90
|
+
|
|
91
|
+
console.print(
|
|
92
|
+
f"[bold cyan]🕵️ Self-Healing 2.0 Otomatik Trafik Yakalama:[/bold cyan] "
|
|
93
|
+
f"[white]{target_url}[/white] ({wait_seconds} sn dinleniyor...)"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async with async_playwright() as p:
|
|
97
|
+
launch_kwargs: dict[str, Any] = {
|
|
98
|
+
"user_data_dir": str(profile_dir.resolve()),
|
|
99
|
+
"headless": True,
|
|
100
|
+
"args": [
|
|
101
|
+
"--disable-blink-features=AutomationControlled",
|
|
102
|
+
"--no-sandbox",
|
|
103
|
+
"--disable-setuid-sandbox",
|
|
104
|
+
"--disable-dev-shm-usage",
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
context = await p.chromium.launch_persistent_context(**launch_kwargs)
|
|
108
|
+
|
|
109
|
+
stored_pw_cookies = self.token_store.get_playwright_cookies(
|
|
110
|
+
tenant_id=self.tenant_id,
|
|
111
|
+
service=service_name,
|
|
112
|
+
default_url=target_url,
|
|
113
|
+
)
|
|
114
|
+
if stored_pw_cookies:
|
|
115
|
+
try:
|
|
116
|
+
await context.add_cookies(stored_pw_cookies)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
page = context.pages[0] if context.pages else await context.new_page()
|
|
121
|
+
|
|
122
|
+
async def on_response(res: Response) -> None:
|
|
123
|
+
nonlocal endpoint_counter
|
|
124
|
+
try:
|
|
125
|
+
req = res.request
|
|
126
|
+
if req.resource_type not in ("xhr", "fetch"):
|
|
127
|
+
return
|
|
128
|
+
req_url = req.url
|
|
129
|
+
if _is_static_asset_url(req_url):
|
|
130
|
+
return
|
|
131
|
+
try:
|
|
132
|
+
parsed_url = urlparse(req_url)
|
|
133
|
+
hostname = (parsed_url.hostname or "").lower()
|
|
134
|
+
except Exception:
|
|
135
|
+
return
|
|
136
|
+
if any(ign in hostname for ign in IGNORED_DOMAIN_KEYWORDS):
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
raw_headers = dict(req.headers)
|
|
141
|
+
except (UnicodeDecodeError, Exception):
|
|
142
|
+
raw_headers = {}
|
|
143
|
+
|
|
144
|
+
filtered_headers: dict[str, str] = {}
|
|
145
|
+
for k, v in raw_headers.items():
|
|
146
|
+
lk = k.lower().strip()
|
|
147
|
+
if any(lk == pref or lk.startswith(pref) for pref in RELEVANT_HEADER_PREFIXES):
|
|
148
|
+
filtered_headers[lk] = v
|
|
149
|
+
|
|
150
|
+
payload: Any = None
|
|
151
|
+
try:
|
|
152
|
+
raw_post = req.post_data
|
|
153
|
+
if raw_post:
|
|
154
|
+
clipped_post = raw_post[:MAX_LLM_FIELD_CHARS]
|
|
155
|
+
try:
|
|
156
|
+
payload = _truncate_for_llm(json.loads(raw_post), max_chars=MAX_LLM_FIELD_CHARS)
|
|
157
|
+
except Exception:
|
|
158
|
+
payload = clipped_post
|
|
159
|
+
except (UnicodeDecodeError, Exception):
|
|
160
|
+
payload = None
|
|
161
|
+
|
|
162
|
+
sample = ""
|
|
163
|
+
is_truncated = False
|
|
164
|
+
try:
|
|
165
|
+
text = await res.text()
|
|
166
|
+
if text:
|
|
167
|
+
sample = text[:MAX_LLM_FIELD_CHARS]
|
|
168
|
+
is_truncated = len(text) > MAX_LLM_FIELD_CHARS
|
|
169
|
+
except (UnicodeDecodeError, Exception):
|
|
170
|
+
sample = ""
|
|
171
|
+
|
|
172
|
+
endpoint_counter += 1
|
|
173
|
+
captured_endpoints.append({
|
|
174
|
+
"id": endpoint_counter,
|
|
175
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
176
|
+
"method": req.method.upper(),
|
|
177
|
+
"url": f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}",
|
|
178
|
+
"full_url": req_url,
|
|
179
|
+
"domain": hostname,
|
|
180
|
+
"path": parsed_url.path,
|
|
181
|
+
"status_code": res.status,
|
|
182
|
+
"resource_type": req.resource_type,
|
|
183
|
+
"query_params": parse_qs(parsed_url.query),
|
|
184
|
+
"headers": filtered_headers,
|
|
185
|
+
"request_payload": payload,
|
|
186
|
+
"response_sample": sample,
|
|
187
|
+
"response_truncated": is_truncated,
|
|
188
|
+
})
|
|
189
|
+
except (UnicodeDecodeError, Exception):
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
page.on("response", on_response)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
await page.goto(target_url, wait_until="domcontentloaded", timeout=20_000)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
console.print(f"[dim]Otomatik gezinme uyarısı ({service_name}): {exc}[/dim]")
|
|
198
|
+
|
|
199
|
+
await asyncio.sleep(wait_seconds)
|
|
200
|
+
|
|
201
|
+
if service_name.lower() == "thy":
|
|
202
|
+
try:
|
|
203
|
+
from registry.thy.client import _build_validate_payload, build_thy_availability_payload
|
|
204
|
+
thy_body = build_thy_availability_payload()
|
|
205
|
+
thy_val = _build_validate_payload(thy_body)
|
|
206
|
+
await page.evaluate(
|
|
207
|
+
"""async ({ valBody, mainBody }) => {
|
|
208
|
+
const hdrs = {
|
|
209
|
+
'Accept': 'application/json, text/plain, */*',
|
|
210
|
+
'Content-Type': 'application/json',
|
|
211
|
+
'Referer': 'https://www.turkishairlines.com/',
|
|
212
|
+
'Origin': 'https://www.turkishairlines.com',
|
|
213
|
+
'x-country': 'TR',
|
|
214
|
+
'x-language': 'tr'
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
await fetch('https://www.turkishairlines.com/api/v1/availability/validate', {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: hdrs,
|
|
220
|
+
credentials: 'include',
|
|
221
|
+
referrer: 'https://www.turkishairlines.com/',
|
|
222
|
+
body: JSON.stringify(valBody)
|
|
223
|
+
});
|
|
224
|
+
} catch (_) {}
|
|
225
|
+
try {
|
|
226
|
+
await fetch('https://www.turkishairlines.com/api/v1/availability', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: hdrs,
|
|
229
|
+
credentials: 'include',
|
|
230
|
+
referrer: 'https://www.turkishairlines.com/',
|
|
231
|
+
body: JSON.stringify(mainBody)
|
|
232
|
+
});
|
|
233
|
+
} catch (_) {}
|
|
234
|
+
}""",
|
|
235
|
+
{"valBody": thy_val, "mainBody": thy_body},
|
|
236
|
+
)
|
|
237
|
+
await asyncio.sleep(1.5)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
cookies = await context.cookies()
|
|
243
|
+
user_agent = await page.evaluate("() => navigator.userAgent")
|
|
244
|
+
self.token_store.save_session(
|
|
245
|
+
tenant_id=self.tenant_id,
|
|
246
|
+
service=service_name,
|
|
247
|
+
cookies=cookies,
|
|
248
|
+
user_agent=user_agent,
|
|
249
|
+
metadata={"target_url": target_url, "auto_healed_at": datetime.now(timezone.utc).isoformat()},
|
|
250
|
+
)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
await context.close()
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
if captured_endpoints:
|
|
260
|
+
traffic_session = {
|
|
261
|
+
"target_url": target_url,
|
|
262
|
+
"recorded_at": datetime.now(timezone.utc).isoformat(),
|
|
263
|
+
"total_endpoints": len(captured_endpoints),
|
|
264
|
+
"endpoints": captured_endpoints,
|
|
265
|
+
}
|
|
266
|
+
output_path.write_text(json.dumps(traffic_session, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
267
|
+
console.print(
|
|
268
|
+
f"[bold green]✔ {service_name}: {len(captured_endpoints)} güncel XHR/Fetch uç noktası yakalandı.[/bold green]"
|
|
269
|
+
)
|
|
270
|
+
return captured_endpoints
|
|
271
|
+
|
|
272
|
+
def _run_auto_capture(self, target_url: str, output_path: Path, service_name: str) -> list[dict[str, Any]]:
|
|
273
|
+
try:
|
|
274
|
+
return asyncio.run(self._auto_capture_fresh_traffic_async(target_url, output_path, service_name))
|
|
275
|
+
except RuntimeError:
|
|
276
|
+
import concurrent.futures
|
|
277
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
278
|
+
return pool.submit(
|
|
279
|
+
asyncio.run,
|
|
280
|
+
self._auto_capture_fresh_traffic_async(target_url, output_path, service_name),
|
|
281
|
+
).result()
|
|
282
|
+
|
|
283
|
+
def validate_all_services(
|
|
284
|
+
self,
|
|
285
|
+
runs: int = 1,
|
|
286
|
+
auto_heal: bool = True,
|
|
287
|
+
services: list[str] | None = None,
|
|
288
|
+
) -> dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
registry/ altındaki tüm aktif servisleri (veya `services` listesini) sırayla test eder.
|
|
291
|
+
Hata alan servisleri otomatik trafik yakalama + GPT-4o 3x doğrulama döngüsüyle tamir eder
|
|
292
|
+
ve raporu `.sessions/auto_repair_report.json` dosyasına yazar.
|
|
293
|
+
"""
|
|
294
|
+
if not self.registry_root.exists():
|
|
295
|
+
return {"total_services": 0, "healthy": 0, "repaired": 0, "failed": 0, "results": []}
|
|
296
|
+
|
|
297
|
+
service_dirs = sorted([
|
|
298
|
+
d for d in self.registry_root.iterdir()
|
|
299
|
+
if d.is_dir() and (d / "schema.json").exists() and (d / "client.py").exists()
|
|
300
|
+
])
|
|
301
|
+
if services:
|
|
302
|
+
wanted = {s.strip().lower() for s in services}
|
|
303
|
+
service_dirs = [d for d in service_dirs if d.name.lower() in wanted]
|
|
304
|
+
|
|
305
|
+
console.print(
|
|
306
|
+
Panel.fit(
|
|
307
|
+
f"[bold cyan]🛠️ Self-Healing 2.0: Toplu Servis Doğrulama & Otomatik Tamir Motoru[/bold cyan]\n"
|
|
308
|
+
f"[white]Taranacak Servis Sayısı:[/white] [bold yellow]{len(service_dirs)}[/bold yellow] | "
|
|
309
|
+
f"[white]Test Tekrarı:[/white] [cyan]{runs}x[/cyan] | "
|
|
310
|
+
f"[white]Otomatik Tamir (Auto-Heal):[/white] [green]{'AKTİF (3x Doğrulama)' if auto_heal else 'KAPALI'}[/green]",
|
|
311
|
+
title="OwnAPI AutoRepairEngine",
|
|
312
|
+
border_style="cyan",
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Arka plan taramasında input() kilitlenmesini önlemek için OWNAPI_NON_INTERACTIVE=1 aç
|
|
317
|
+
prev_ni = os.environ.get("OWNAPI_NON_INTERACTIVE")
|
|
318
|
+
prev_pw = os.environ.get("OWNAPI_HITL_PAUSE_AND_WAIT")
|
|
319
|
+
os.environ["OWNAPI_NON_INTERACTIVE"] = "1"
|
|
320
|
+
os.environ["OWNAPI_HITL_PAUSE_AND_WAIT"] = "0"
|
|
321
|
+
|
|
322
|
+
service_reports: list[dict[str, Any]] = []
|
|
323
|
+
healthy_count = 0
|
|
324
|
+
repaired_count = 0
|
|
325
|
+
pending_hitl_count = 0
|
|
326
|
+
failed_count = 0
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
for sdir in service_dirs:
|
|
330
|
+
svc_name = sdir.name
|
|
331
|
+
schema_path = sdir / "schema.json"
|
|
332
|
+
client_path = sdir / "client.py"
|
|
333
|
+
traffic_path = sdir / "captured_traffic.json"
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
raw_schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
337
|
+
schema_obj = RegistrySchema.model_validate(raw_schema)
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
failed_count += 1
|
|
340
|
+
service_reports.append({
|
|
341
|
+
"service_name": svc_name,
|
|
342
|
+
"initial_status": "BAŞARISIZ",
|
|
343
|
+
"final_status": "BAŞARISIZ",
|
|
344
|
+
"auto_healed": False,
|
|
345
|
+
"detail": f"Şema okuma hatası: {exc}",
|
|
346
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
347
|
+
})
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
console.print(f"\n[bold magenta]🔍 [{svc_name}][/bold magenta] `{schema_obj.function_name}()` test ediliyor...")
|
|
351
|
+
passed = 0
|
|
352
|
+
last_detail = ""
|
|
353
|
+
is_pending_hitl = False
|
|
354
|
+
hitl_task_id = None
|
|
355
|
+
start_t = time.perf_counter()
|
|
356
|
+
|
|
357
|
+
for r_idx in range(1, runs + 1):
|
|
358
|
+
try:
|
|
359
|
+
res = _load_and_call_client(
|
|
360
|
+
client_path=client_path,
|
|
361
|
+
function_name=schema_obj.function_name,
|
|
362
|
+
tenant_id=self.tenant_id,
|
|
363
|
+
mode=self.mode,
|
|
364
|
+
)
|
|
365
|
+
if isinstance(res, dict) and str(res.get("status", "")).upper() in ("PAUSED", "PENDING_USER_APPROVAL", "PENDING_USER_ACTION"):
|
|
366
|
+
is_pending_hitl = True
|
|
367
|
+
hitl_task_id = res.get("task_id")
|
|
368
|
+
last_detail = f"HITL Kuyruğunda PAUSED / Onay Bekliyor (task_id={hitl_task_id})"
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
ok, detail = _is_valid_json_response(res)
|
|
372
|
+
if ok:
|
|
373
|
+
passed += 1
|
|
374
|
+
last_detail = detail
|
|
375
|
+
else:
|
|
376
|
+
last_detail = detail
|
|
377
|
+
break
|
|
378
|
+
except Exception as exc:
|
|
379
|
+
last_detail = f"Çalışma zamanı hatası: {exc}"
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
383
|
+
|
|
384
|
+
if is_pending_hitl:
|
|
385
|
+
pending_hitl_count += 1
|
|
386
|
+
service_reports.append({
|
|
387
|
+
"service_name": svc_name,
|
|
388
|
+
"function_name": schema_obj.function_name,
|
|
389
|
+
"initial_status": "PENDING_USER_APPROVAL",
|
|
390
|
+
"final_status": "PENDING_USER_APPROVAL",
|
|
391
|
+
"hitl_task_id": hitl_task_id,
|
|
392
|
+
"auto_healed": False,
|
|
393
|
+
"elapsed_ms": elapsed_ms,
|
|
394
|
+
"detail": last_detail,
|
|
395
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
396
|
+
})
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
if passed == runs:
|
|
400
|
+
healthy_count += 1
|
|
401
|
+
raw_schema["validation_status"] = "BAŞARILI"
|
|
402
|
+
raw_schema["last_validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
403
|
+
schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
404
|
+
service_reports.append({
|
|
405
|
+
"service_name": svc_name,
|
|
406
|
+
"function_name": schema_obj.function_name,
|
|
407
|
+
"initial_status": "BAŞARILI",
|
|
408
|
+
"final_status": "BAŞARILI",
|
|
409
|
+
"auto_healed": False,
|
|
410
|
+
"elapsed_ms": elapsed_ms,
|
|
411
|
+
"detail": last_detail,
|
|
412
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
413
|
+
})
|
|
414
|
+
console.print(f" [bold green]✔ {svc_name} Sağlıklı ({elapsed_ms:.0f} ms)[/bold green] — {last_detail}")
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
# Servis testten geçemedi -> Auto-Heal (Trafik Yakalama + GPT-4o Onarım + 3x Doğrulama)
|
|
418
|
+
console.print(f" [bold red]✖ {svc_name} Test Başarısız:[/bold red] {last_detail}")
|
|
419
|
+
raw_schema["validation_status"] = "BAŞARISIZ"
|
|
420
|
+
|
|
421
|
+
if not auto_heal:
|
|
422
|
+
failed_count += 1
|
|
423
|
+
schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
424
|
+
service_reports.append({
|
|
425
|
+
"service_name": svc_name,
|
|
426
|
+
"function_name": schema_obj.function_name,
|
|
427
|
+
"initial_status": "BAŞARISIZ",
|
|
428
|
+
"final_status": "BAŞARISIZ",
|
|
429
|
+
"auto_healed": False,
|
|
430
|
+
"elapsed_ms": elapsed_ms,
|
|
431
|
+
"detail": last_detail,
|
|
432
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
433
|
+
})
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
console.print(f" [bold yellow]🔧 [{svc_name}] Self-Healing 2.0 Otomatik Onarım Başlatılıyor...[/bold yellow]")
|
|
437
|
+
endpoints: list[dict[str, Any]] = []
|
|
438
|
+
if traffic_path.exists():
|
|
439
|
+
try:
|
|
440
|
+
tdata = json.loads(traffic_path.read_text(encoding="utf-8"))
|
|
441
|
+
endpoints = tdata.get("endpoints", [])
|
|
442
|
+
except Exception:
|
|
443
|
+
endpoints = []
|
|
444
|
+
|
|
445
|
+
# Hedef URL'ye gidip taze trafik yakala
|
|
446
|
+
fresh_eps = self._run_auto_capture(schema_obj.target_url, traffic_path, svc_name)
|
|
447
|
+
if fresh_eps:
|
|
448
|
+
endpoints = fresh_eps
|
|
449
|
+
|
|
450
|
+
meaningful = _select_meaningful_endpoints(endpoints)
|
|
451
|
+
raw_matched = next(
|
|
452
|
+
(e for e in meaningful if e.get("url") == schema_obj.endpoint_url),
|
|
453
|
+
meaningful[0] if meaningful else (endpoints[0] if endpoints else {"url": schema_obj.endpoint_url, "method": schema_obj.method}),
|
|
454
|
+
)
|
|
455
|
+
matched_ep = _sanitize_endpoint_for_llm(raw_matched, max_chars=MAX_LLM_FIELD_CHARS)
|
|
456
|
+
|
|
457
|
+
# Mevcut client.py dosyasında SyntaxError varsa önce _compile_with_llm ile temiz yeniden derle
|
|
458
|
+
try:
|
|
459
|
+
import ast
|
|
460
|
+
|
|
461
|
+
ast.parse(client_path.read_text(encoding="utf-8"))
|
|
462
|
+
except Exception:
|
|
463
|
+
try:
|
|
464
|
+
recompiled_code, recompiled_schema, matched_ep = _compile_with_llm(
|
|
465
|
+
svc_name,
|
|
466
|
+
schema_obj.target_url,
|
|
467
|
+
schema_obj.goal,
|
|
468
|
+
meaningful or endpoints,
|
|
469
|
+
)
|
|
470
|
+
schema_obj.function_name = recompiled_schema.function_name
|
|
471
|
+
client_path.write_text(recompiled_code.strip() + "\n", encoding="utf-8")
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
|
|
475
|
+
repair_report = validate_and_repair_client(
|
|
476
|
+
service_name=svc_name,
|
|
477
|
+
target_url=schema_obj.target_url,
|
|
478
|
+
description=schema_obj.goal,
|
|
479
|
+
client_path=client_path,
|
|
480
|
+
schema_obj=schema_obj,
|
|
481
|
+
matched_endpoint=matched_ep,
|
|
482
|
+
all_endpoints=meaningful or endpoints,
|
|
483
|
+
validation_runs=3,
|
|
484
|
+
max_repair_attempts=3,
|
|
485
|
+
tenant_id=self.tenant_id,
|
|
486
|
+
mode=self.mode,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
final_st = repair_report.get("validation_status", "BAŞARISIZ")
|
|
490
|
+
if final_st == "BAŞARILI":
|
|
491
|
+
repaired_count += 1
|
|
492
|
+
raw_schema["validation_status"] = "BAŞARILI"
|
|
493
|
+
raw_schema["validation_passed_runs"] = 3
|
|
494
|
+
else:
|
|
495
|
+
failed_count += 1
|
|
496
|
+
raw_schema["validation_status"] = "BAŞARISIZ"
|
|
497
|
+
|
|
498
|
+
raw_schema["last_validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
499
|
+
schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
500
|
+
|
|
501
|
+
service_reports.append({
|
|
502
|
+
"service_name": svc_name,
|
|
503
|
+
"function_name": schema_obj.function_name,
|
|
504
|
+
"initial_status": "BAŞARISIZ",
|
|
505
|
+
"final_status": final_st,
|
|
506
|
+
"auto_healed": final_st == "BAŞARILI",
|
|
507
|
+
"repair_attempts": repair_report.get("repair_attempts", 0),
|
|
508
|
+
"elapsed_ms": elapsed_ms,
|
|
509
|
+
"detail": "3/3 Auto-Repair Başarılı" if final_st == "BAŞARILI" else repair_report.get("last_error", last_detail),
|
|
510
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
511
|
+
})
|
|
512
|
+
finally:
|
|
513
|
+
if prev_ni is None:
|
|
514
|
+
os.environ.pop("OWNAPI_NON_INTERACTIVE", None)
|
|
515
|
+
else:
|
|
516
|
+
os.environ["OWNAPI_NON_INTERACTIVE"] = prev_ni
|
|
517
|
+
if prev_pw is None:
|
|
518
|
+
os.environ.pop("OWNAPI_HITL_PAUSE_AND_WAIT", None)
|
|
519
|
+
else:
|
|
520
|
+
os.environ["OWNAPI_HITL_PAUSE_AND_WAIT"] = prev_pw
|
|
521
|
+
|
|
522
|
+
summary_report = {
|
|
523
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
524
|
+
"tenant_id": self.tenant_id,
|
|
525
|
+
"mode": self.mode,
|
|
526
|
+
"total_services": len(service_dirs),
|
|
527
|
+
"healthy": healthy_count,
|
|
528
|
+
"repaired": repaired_count,
|
|
529
|
+
"pending_hitl": pending_hitl_count,
|
|
530
|
+
"failed": failed_count,
|
|
531
|
+
"results": service_reports,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
self._append_report_to_disk(summary_report)
|
|
535
|
+
self._print_summary_table(summary_report)
|
|
536
|
+
return summary_report
|
|
537
|
+
|
|
538
|
+
def _append_report_to_disk(self, report: dict[str, Any]) -> None:
|
|
539
|
+
"""Tamir ve sağlık geçmişini `.sessions/auto_repair_report.json` dosyasına zaman damgasıyla kaydeder."""
|
|
540
|
+
history: list[dict[str, Any]] = []
|
|
541
|
+
if self.report_path.exists():
|
|
542
|
+
try:
|
|
543
|
+
existing = json.loads(self.report_path.read_text(encoding="utf-8"))
|
|
544
|
+
if isinstance(existing, dict) and isinstance(existing.get("history"), list):
|
|
545
|
+
history = existing["history"]
|
|
546
|
+
except Exception:
|
|
547
|
+
history = []
|
|
548
|
+
|
|
549
|
+
history.append(report)
|
|
550
|
+
payload = {
|
|
551
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
552
|
+
"total_runs": len(history),
|
|
553
|
+
"latest_summary": {
|
|
554
|
+
"timestamp": report["timestamp"],
|
|
555
|
+
"total_services": report["total_services"],
|
|
556
|
+
"healthy": report["healthy"],
|
|
557
|
+
"repaired": report["repaired"],
|
|
558
|
+
"pending_hitl": report["pending_hitl"],
|
|
559
|
+
"failed": report["failed"],
|
|
560
|
+
},
|
|
561
|
+
"history": history[-50:],
|
|
562
|
+
}
|
|
563
|
+
self.report_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
564
|
+
|
|
565
|
+
def _print_summary_table(self, report: dict[str, Any]) -> None:
|
|
566
|
+
table = Table(
|
|
567
|
+
title=(
|
|
568
|
+
f"🩺 OwnAPI Self-Healing 2.0 Sağlık & Tamir Raporu "
|
|
569
|
+
f"(Sağlıklı: {report['healthy']} | Onarılan: {report['repaired']} | "
|
|
570
|
+
f"HITL Bekleyen: {report['pending_hitl']} | Hatalı: {report['failed']})"
|
|
571
|
+
),
|
|
572
|
+
show_header=True,
|
|
573
|
+
header_style="bold magenta",
|
|
574
|
+
show_lines=True,
|
|
575
|
+
)
|
|
576
|
+
table.add_column("Servis", style="bold cyan", no_wrap=True)
|
|
577
|
+
table.add_column("İlk Durum", justify="center")
|
|
578
|
+
table.add_column("Son Durum", justify="center")
|
|
579
|
+
table.add_column("Otomatik Tamir", justify="center")
|
|
580
|
+
table.add_column("Süre", justify="right", style="yellow")
|
|
581
|
+
table.add_column("Açıklama / Detay", style="white", overflow="fold")
|
|
582
|
+
|
|
583
|
+
for item in report.get("results", []):
|
|
584
|
+
init_st = item.get("initial_status", "-")
|
|
585
|
+
fin_st = item.get("final_status", "-")
|
|
586
|
+
init_badge = "[green]🟢 BAŞARILI[/green]" if init_st == "BAŞARILI" else (
|
|
587
|
+
"[yellow]⏳ PENDING_HITL[/yellow]" if init_st == "PENDING_USER_APPROVAL" else "[red]🔴 BAŞARISIZ[/red]"
|
|
588
|
+
)
|
|
589
|
+
fin_badge = "[bold green]🟢 BAŞARILI[/bold green]" if fin_st == "BAŞARILI" else (
|
|
590
|
+
"[bold yellow]⏳ PENDING_HITL[/bold yellow]" if fin_st == "PENDING_USER_APPROVAL" else "[bold red]🔴 BAŞARISIZ[/bold red]"
|
|
591
|
+
)
|
|
592
|
+
healed_badge = "[bold cyan]🛠️ Onarıldı (3/3)[/bold cyan]" if item.get("auto_healed") else "[dim]-[/dim]"
|
|
593
|
+
ms_str = f"{item.get('elapsed_ms', 0):.0f} ms"
|
|
594
|
+
table.add_row(
|
|
595
|
+
str(item.get("service_name")),
|
|
596
|
+
init_badge,
|
|
597
|
+
fin_badge,
|
|
598
|
+
healed_badge,
|
|
599
|
+
ms_str,
|
|
600
|
+
str(item.get("detail", ""))[:140],
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
console.print()
|
|
604
|
+
console.print(table)
|
|
605
|
+
console.print(f"[dim]Rapor kaydedildi: {self.report_path}[/dim]\n")
|
|
606
|
+
|
|
607
|
+
@staticmethod
|
|
608
|
+
def _seconds_until_schedule(schedule_time: str = "03:00") -> float:
|
|
609
|
+
"""HH:MM formatındaki hedef saate kalan saniyeyi hesaplar."""
|
|
610
|
+
now = datetime.now()
|
|
611
|
+
try:
|
|
612
|
+
hour, minute = [int(x) for x in schedule_time.strip().split(":", 1)]
|
|
613
|
+
except Exception:
|
|
614
|
+
hour, minute = 3, 0
|
|
615
|
+
target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
616
|
+
if target <= now:
|
|
617
|
+
target += timedelta(days=1)
|
|
618
|
+
return max(1.0, (target - now).total_seconds())
|
|
619
|
+
|
|
620
|
+
def run_daemon(
|
|
621
|
+
self,
|
|
622
|
+
schedule_time: str = "03:00",
|
|
623
|
+
interval_minutes: float | None = None,
|
|
624
|
+
runs: int = 1,
|
|
625
|
+
auto_heal: bool = True,
|
|
626
|
+
run_immediately: bool = False,
|
|
627
|
+
max_cycles: int | None = None,
|
|
628
|
+
) -> None:
|
|
629
|
+
"""
|
|
630
|
+
Her gece `schedule_time` (varsayılan '03:00') saatinde veya `interval_minutes`
|
|
631
|
+
dakika periyodunda çalışan hafif arka plan bakım ve otomatik tamir zamanlayıcısı.
|
|
632
|
+
"""
|
|
633
|
+
schedule_desc = (
|
|
634
|
+
f"Her {interval_minutes} dakikada bir"
|
|
635
|
+
if interval_minutes
|
|
636
|
+
else f"Her gece saat {schedule_time}'te"
|
|
637
|
+
)
|
|
638
|
+
console.print(
|
|
639
|
+
Panel.fit(
|
|
640
|
+
f"[bold green]🌙 OwnAPI Self-Healing 2.0 Gece Bakım Daemon'u Başlatıldı[/bold green]\n"
|
|
641
|
+
f"[white]Zamanlama:[/white] [bold cyan]{schedule_desc}[/bold cyan]\n"
|
|
642
|
+
f"[white]Otomatik Tamir (GPT-4o + 3x Auto-Test):[/white] [green]{'Aktif' if auto_heal else 'Pasif'}[/green]\n"
|
|
643
|
+
f"[white]Rapor Dosyası:[/white] [dim]{self.report_path}[/dim]\n"
|
|
644
|
+
"[dim]Durdurmak için CTRL+C tuşlarına basın.[/dim]",
|
|
645
|
+
title="OwnAPI Auto-Repair Daemon",
|
|
646
|
+
border_style="green",
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
cycle = 0
|
|
651
|
+
if run_immediately:
|
|
652
|
+
cycle += 1
|
|
653
|
+
console.print(f"\n[bold cyan]⚡ İlk bakım döngüsü (#{cycle}) hemen çalıştırılıyor...[/bold cyan]")
|
|
654
|
+
self.validate_all_services(runs=runs, auto_heal=auto_heal)
|
|
655
|
+
if max_cycles and cycle >= max_cycles:
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
try:
|
|
659
|
+
while True:
|
|
660
|
+
if interval_minutes is not None and interval_minutes > 0:
|
|
661
|
+
wait_sec = float(interval_minutes) * 60.0
|
|
662
|
+
else:
|
|
663
|
+
wait_sec = self._seconds_until_schedule(schedule_time)
|
|
664
|
+
|
|
665
|
+
next_run_dt = datetime.now() + timedelta(seconds=wait_sec)
|
|
666
|
+
console.print(
|
|
667
|
+
f"[dim]⏳ Sonraki otomatik bakım zamanı: "
|
|
668
|
+
f"{next_run_dt.strftime('%Y-%m-%d %H:%M:%S')} ({wait_sec / 60.0:.1f} dk sonra)...[/dim]"
|
|
669
|
+
)
|
|
670
|
+
time.sleep(wait_sec)
|
|
671
|
+
|
|
672
|
+
cycle += 1
|
|
673
|
+
console.print(
|
|
674
|
+
f"\n[bold cyan]🛠️ Zamanlanmış Gece Bakımı Döngüsü (#{cycle}) Başladı "
|
|
675
|
+
f"({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})[/bold cyan]"
|
|
676
|
+
)
|
|
677
|
+
self.validate_all_services(runs=runs, auto_heal=auto_heal)
|
|
678
|
+
|
|
679
|
+
if max_cycles and cycle >= max_cycles:
|
|
680
|
+
break
|
|
681
|
+
except KeyboardInterrupt:
|
|
682
|
+
console.print("\n[yellow]⏹️ OwnAPI Auto-Repair Daemon kullanıcı tarafından durduruldu.[/yellow]")
|