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
server.py
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
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, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import Body, FastAPI, Header, HTTPException, Query
|
|
13
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
14
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
15
|
+
from fastapi.staticfiles import StaticFiles
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
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.api_keys import (
|
|
24
|
+
PLAN_CATALOG,
|
|
25
|
+
SITE_LIMIT_WARNING_MESSAGE,
|
|
26
|
+
SiteLimitExceededError,
|
|
27
|
+
consume_credit,
|
|
28
|
+
generate_api_key,
|
|
29
|
+
get_or_create_tenant,
|
|
30
|
+
get_tenant_info,
|
|
31
|
+
register_site_usage,
|
|
32
|
+
upgrade_plan,
|
|
33
|
+
)
|
|
34
|
+
from core.auto_repair import AutoRepairEngine
|
|
35
|
+
from core.consent import USER_DELEGATION_DISCLAIMER, TOS_VERSION, ConsentManager
|
|
36
|
+
from core.hitl_queue import HITLQueueManager
|
|
37
|
+
from core.onboarding import onboard_new_site, validate_and_repair_client
|
|
38
|
+
from core.token_store import TokenStore
|
|
39
|
+
from own_api.sdk import OwnAPIClient
|
|
40
|
+
|
|
41
|
+
STATIC_DIR = PROJECT_ROOT / "static"
|
|
42
|
+
STATIC_DIR.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
app = FastAPI(
|
|
45
|
+
title="OwnAPI Enterprise REST Server & Live Playground",
|
|
46
|
+
description=(
|
|
47
|
+
"Plaid-Style User-Delegated Browser Middleware, 3x Auto-Validated Registry Moat, "
|
|
48
|
+
"Self-Healing 2.0 Nightly Auto-Repair & Human-in-the-Loop (HITL) Queue API."
|
|
49
|
+
),
|
|
50
|
+
version="1.0.0",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
app.add_middleware(
|
|
54
|
+
CORSMiddleware,
|
|
55
|
+
allow_origins=["*"],
|
|
56
|
+
allow_credentials=True,
|
|
57
|
+
allow_methods=["*"],
|
|
58
|
+
allow_headers=["*"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CallRequest(BaseModel):
|
|
63
|
+
service: str = Field(..., description="Çağrılacak kayıtlı servis adı (örn: trendyol, enuygun, wikipedia, thy)")
|
|
64
|
+
params: dict[str, Any] = Field(default_factory=dict, description="Servise iletilecek dinamik parametreler")
|
|
65
|
+
tenant_id: str = Field(default="default", description="Multi-tenant oturum izolasyonu kimliği")
|
|
66
|
+
api_key: str | None = Field(default=None, description="Opsiyonel sk-ownapi-... API anahtarı")
|
|
67
|
+
mode: str | None = Field(default=None, description="Çalışma modu: 'local' (User Chrome) veya 'cloud' (Docker/CDP)")
|
|
68
|
+
non_interactive: bool = Field(
|
|
69
|
+
default=False,
|
|
70
|
+
description="True ise Captcha/WAF durumunda görevi HITL onay kuyruğuna ekler",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AskRequest(BaseModel):
|
|
75
|
+
prompt: str = Field(..., description="Doğal dil ajanına sorulacak soru veya talimat")
|
|
76
|
+
service: str | None = Field(default=None, description="Opsiyonel hedef site/konektör adı")
|
|
77
|
+
tenant_id: str = Field(default="default", description="Kiracı ID'si")
|
|
78
|
+
api_key: str | None = Field(default=None, description="Opsiyonel sk-ownapi-... API anahtarı")
|
|
79
|
+
mode: str | None = Field(default=None, description="Çalışma modu: 'local' veya 'cloud'")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ValidateRequest(BaseModel):
|
|
83
|
+
runs: int = Field(default=3, ge=1, le=10, description="Ardışık doğrulama testi sayısı (varsayılan: 3)")
|
|
84
|
+
tenant_id: str = Field(default="default", description="Kiracı ID'si")
|
|
85
|
+
mode: str = Field(default="local", description="Çalışma modu: 'local' veya 'cloud'")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AutoRepairRequest(BaseModel):
|
|
89
|
+
runs: int = Field(default=1, ge=1, le=5, description="Her servis için yapılacak sağlık testi sayısı")
|
|
90
|
+
auto_heal: bool = Field(default=True, description="Başarısız servisleri otomatik tamir et")
|
|
91
|
+
services: list[str] | None = Field(default=None, description="Belirli servis alt kümesi (None ise tümü)")
|
|
92
|
+
tenant_id: str = Field(default="default", description="Kiracı ID'si")
|
|
93
|
+
mode: str = Field(default="local", description="Çalışma modu: 'local' veya 'cloud'")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OnboardRequest(BaseModel):
|
|
97
|
+
service_name: str = Field(..., description="Eklenecek yeni servis adı (örn: biletix, stripe_docs)")
|
|
98
|
+
target_url: str = Field(..., description="Hedef web sitesi URL'si (örn: https://www.biletix.com)")
|
|
99
|
+
description: str = Field(default="", description="Servisin amacı / yapılacak işlem açıklaması")
|
|
100
|
+
consent_accepted: bool = Field(
|
|
101
|
+
default=True,
|
|
102
|
+
description="Plaid tarzı Yasal Onay: Kullanıcının kendi hesabı ve tarayıcısıyla vekaleten eriştiğinin onayı",
|
|
103
|
+
)
|
|
104
|
+
tenant_id: str = Field(default="default", description="Kiracı ID'si")
|
|
105
|
+
mode: str = Field(default="local", description="Çalışma modu: 'local' veya 'cloud'")
|
|
106
|
+
wait_seconds: int = Field(default=20, ge=3, le=120, description="Tarayıcı kayıt süresi (sn)")
|
|
107
|
+
validation_runs: int = Field(default=3, ge=1, le=5, description="Auto-Test doğrulama tekrarı (varsayılan: 3)")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class GenerateKeyRequest(BaseModel):
|
|
111
|
+
tenant_id: str = Field(default="default", description="Anahtar üretilecek kiracı ID'si")
|
|
112
|
+
plan: str | None = Field(default=None, description="Paket tipi (None ise mevcut paketi korunur)")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class BillingUpgradeRequest(BaseModel):
|
|
116
|
+
tenant_id: str = Field(default="default", description="Yükseltilecek kiracı ID'si")
|
|
117
|
+
plan: str = Field(default="starter_cloud", description="Hedef paket: 'starter_cloud' ($10), 'growth_cloud' ($49) veya 'free'")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class HITLResolveRequest(BaseModel):
|
|
121
|
+
otp_code: str | None = Field(default=None, description="Opsiyonel SMS / E-Posta / 2FA doğrulama kodu (örn: 123456)")
|
|
122
|
+
tenant_id: str | None = Field(default=None, description="Opsiyonel kiracı ID'si")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _mask_api_key(api_key: str) -> str:
|
|
126
|
+
clean = str(api_key or "").strip()
|
|
127
|
+
if len(clean) <= 16:
|
|
128
|
+
return clean
|
|
129
|
+
return f"{clean[:10]}••••••••••••••••{clean[-6:]}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_stripe_payment_links() -> dict[str, str | None]:
|
|
133
|
+
"""
|
|
134
|
+
Ortam değişkenlerinde tanımlı gerçek Stripe Payment Link adreslerini döndürür:
|
|
135
|
+
- `STRIPE_STARTER_LINK` ($10 Starter Cloud)
|
|
136
|
+
- `STRIPE_GROWTH_LINK` ($49 Growth Cloud)
|
|
137
|
+
Tanımlı değilse `None` döner ve yerel simülasyon modu kullanılır.
|
|
138
|
+
"""
|
|
139
|
+
starter_link = os.getenv("STRIPE_STARTER_LINK", "").strip() or None
|
|
140
|
+
growth_link = os.getenv("STRIPE_GROWTH_LINK", "").strip() or None
|
|
141
|
+
return {
|
|
142
|
+
"starter_cloud": starter_link,
|
|
143
|
+
"growth_cloud": growth_link,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _build_account_payload(tenant_id: str = "default") -> dict[str, Any]:
|
|
148
|
+
rec = get_tenant_info(tenant_id)
|
|
149
|
+
plan_key = str(rec.get("plan", "free"))
|
|
150
|
+
plan_meta = PLAN_CATALOG.get(plan_key, PLAN_CATALOG["free"])
|
|
151
|
+
max_credits = int(plan_meta.get("credits", 1000))
|
|
152
|
+
rem_credits = int(rec.get("credits", max_credits))
|
|
153
|
+
plan_used = max(0, max_credits - rem_credits)
|
|
154
|
+
consumed = int(rec.get("total_consumed", plan_used))
|
|
155
|
+
full_key = str(rec.get("api_key", ""))
|
|
156
|
+
max_sites = int(rec.get("max_sites", plan_meta.get("max_sites", 2)))
|
|
157
|
+
active_sites = list(rec.get("active_sites", [])) if isinstance(rec.get("active_sites"), list) else []
|
|
158
|
+
stripe_links = _get_stripe_payment_links()
|
|
159
|
+
return {
|
|
160
|
+
"success": True,
|
|
161
|
+
"tenant_id": rec.get("tenant_id", tenant_id),
|
|
162
|
+
"plan": plan_key,
|
|
163
|
+
"plan_label": rec.get("plan_label", plan_meta["label"]),
|
|
164
|
+
"mode": rec.get("mode", plan_meta["mode"]),
|
|
165
|
+
"price_usd": int(rec.get("price_usd", plan_meta["price_usd"])),
|
|
166
|
+
"credits": rem_credits,
|
|
167
|
+
"remaining_credits": rem_credits,
|
|
168
|
+
"max_credits": max_credits,
|
|
169
|
+
"total_credits": max_credits,
|
|
170
|
+
"used_credits": plan_used,
|
|
171
|
+
"total_consumed": consumed,
|
|
172
|
+
"max_sites": max_sites,
|
|
173
|
+
"active_sites": active_sites,
|
|
174
|
+
"active_sites_count": len(active_sites),
|
|
175
|
+
"api_key": full_key,
|
|
176
|
+
"masked_api_key": _mask_api_key(full_key),
|
|
177
|
+
"stripe_starter_link": stripe_links["starter_cloud"],
|
|
178
|
+
"stripe_growth_link": stripe_links["growth_cloud"],
|
|
179
|
+
"stripe_links": stripe_links,
|
|
180
|
+
"created_at": rec.get("created_at"),
|
|
181
|
+
"updated_at": rec.get("updated_at"),
|
|
182
|
+
"plans": PLAN_CATALOG,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.get("/api/v1/account")
|
|
187
|
+
async def get_account_endpoint(tenant_id: str = Query(default="default")) -> dict[str, Any]:
|
|
188
|
+
"""
|
|
189
|
+
Kullanıcının mevcut paketini (`free`, `starter_cloud`, `growth_cloud`),
|
|
190
|
+
kalan/toplam kredisini, çalışma modunu (`local`/`cloud`) ve maskelenmiş/tam
|
|
191
|
+
`sk-ownapi-...` anahtarını döndürür.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
return _build_account_payload(tenant_id=tenant_id)
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.post("/api/v1/keys/generate")
|
|
200
|
+
async def generate_key_endpoint(req: GenerateKeyRequest | None = None) -> dict[str, Any]:
|
|
201
|
+
"""Kullanıcıya yeni bir `sk-ownapi-...` API anahtarı üretir ve döndürür."""
|
|
202
|
+
cfg = req or GenerateKeyRequest()
|
|
203
|
+
try:
|
|
204
|
+
current = get_tenant_info(cfg.tenant_id)
|
|
205
|
+
target_plan = (cfg.plan or current.get("plan") or "free").strip().lower()
|
|
206
|
+
new_key = generate_api_key(tenant_id=current.get("tenant_id", cfg.tenant_id), plan=target_plan)
|
|
207
|
+
payload = _build_account_payload(tenant_id=new_key.tenant_id)
|
|
208
|
+
payload["message"] = "Yeni API anahtarı başarıyla üretildi."
|
|
209
|
+
return payload
|
|
210
|
+
except ValueError as exc:
|
|
211
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.post("/api/v1/billing/upgrade")
|
|
215
|
+
async def billing_upgrade_endpoint(req: BillingUpgradeRequest) -> dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Kullanıcının seçtiği pakete (`starter_cloud` $10 / 10.000 kredi veya
|
|
218
|
+
`growth_cloud` $49 / 100.000 kredi) geçişini yapar, yeni `sk-ownapi-...`
|
|
219
|
+
anahtarını ve `mode="cloud"` bilgisini döndürür.
|
|
220
|
+
Eğer `STRIPE_STARTER_LINK` veya `STRIPE_GROWTH_LINK` ortam değişkeni tanımlıysa
|
|
221
|
+
`stripe_checkout_url` alanıyla gerçek Stripe ödeme bağlantısını da döndürür.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
clean_plan = (req.plan or "starter_cloud").strip().lower()
|
|
225
|
+
stripe_links = _get_stripe_payment_links()
|
|
226
|
+
stripe_checkout_url = stripe_links.get(clean_plan)
|
|
227
|
+
|
|
228
|
+
new_key = upgrade_plan(tenant_id=req.tenant_id, plan=clean_plan)
|
|
229
|
+
payload = _build_account_payload(tenant_id=new_key.tenant_id)
|
|
230
|
+
payload["stripe_checkout_url"] = stripe_checkout_url
|
|
231
|
+
payload["redirect_url"] = stripe_checkout_url
|
|
232
|
+
payload["payment_mode"] = "stripe" if stripe_checkout_url else "simulation"
|
|
233
|
+
payload["message"] = "Tebrikler! Artık buluttasın"
|
|
234
|
+
return payload
|
|
235
|
+
except ValueError as exc:
|
|
236
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.get("/", response_class=HTMLResponse)
|
|
240
|
+
async def serve_dashboard() -> HTMLResponse:
|
|
241
|
+
"""OwnAPI Enterprise SaaS Web Dashboard & Live Playground arayüzünü sunar."""
|
|
242
|
+
index_file = STATIC_DIR / "index.html"
|
|
243
|
+
if not index_file.exists():
|
|
244
|
+
return HTMLResponse("<h1>OwnAPI Dashboard bulunamadı (static/index.html)</h1>", status_code=404)
|
|
245
|
+
return HTMLResponse(index_file.read_text(encoding="utf-8"))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.get("/api/v1/services")
|
|
249
|
+
async def list_services(tenant_id: str = Query(default="default")) -> dict[str, Any]:
|
|
250
|
+
"""
|
|
251
|
+
Kayıtlı tüm `registry/` servislerini, şemalarını, `validation_status` (3/3 Auto-Test durumu),
|
|
252
|
+
parametrelerini, TokenStore oturum ve ToS onay durumlarını döndürür.
|
|
253
|
+
"""
|
|
254
|
+
client = OwnAPIClient(tenant_id=tenant_id)
|
|
255
|
+
token_store = TokenStore()
|
|
256
|
+
consent_mgr = ConsentManager()
|
|
257
|
+
hitl_mgr = HITLQueueManager()
|
|
258
|
+
|
|
259
|
+
base_services = client.list_services()
|
|
260
|
+
enriched: list[dict[str, Any]] = []
|
|
261
|
+
validated_count = 0
|
|
262
|
+
active_sessions = 0
|
|
263
|
+
|
|
264
|
+
for svc in base_services:
|
|
265
|
+
s_name = svc["service_name"]
|
|
266
|
+
schema_path = PROJECT_ROOT / "registry" / s_name / "schema.json"
|
|
267
|
+
val_status = "BAŞARILI"
|
|
268
|
+
val_runs = 3
|
|
269
|
+
last_val_at = None
|
|
270
|
+
target_url = ""
|
|
271
|
+
|
|
272
|
+
if schema_path.exists():
|
|
273
|
+
try:
|
|
274
|
+
raw_schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
275
|
+
val_status = raw_schema.get("validation_status", "BAŞARILI")
|
|
276
|
+
val_runs = raw_schema.get("validation_passed_runs", 3)
|
|
277
|
+
last_val_at = raw_schema.get("last_validated_at") or raw_schema.get("created_at")
|
|
278
|
+
target_url = raw_schema.get("target_url", "")
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
if val_status == "BAŞARILI":
|
|
283
|
+
validated_count += 1
|
|
284
|
+
|
|
285
|
+
sess = token_store.get_session(tenant_id, s_name)
|
|
286
|
+
has_sess = bool(sess and (sess.get("cookie") or sess.get("playwright_cookies")))
|
|
287
|
+
if has_sess:
|
|
288
|
+
active_sessions += 1
|
|
289
|
+
|
|
290
|
+
has_consent = consent_mgr.has_consented(tenant_id, s_name)
|
|
291
|
+
|
|
292
|
+
enriched.append({
|
|
293
|
+
**svc,
|
|
294
|
+
"target_url": target_url,
|
|
295
|
+
"validation_status": val_status,
|
|
296
|
+
"validation_passed_runs": val_runs,
|
|
297
|
+
"last_validated_at": last_val_at,
|
|
298
|
+
"has_active_session": has_sess,
|
|
299
|
+
"session_updated_at": sess.get("updated_at") or sess.get("healed_at") if sess else None,
|
|
300
|
+
"has_tos_consent": has_consent,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
pending_tasks = hitl_mgr.list_tasks(tenant_id=None, status_filter="PENDING_USER_ACTION")
|
|
304
|
+
|
|
305
|
+
auto_repair_file = PROJECT_ROOT / ".sessions" / "auto_repair_report.json"
|
|
306
|
+
latest_repair = None
|
|
307
|
+
if auto_repair_file.exists():
|
|
308
|
+
try:
|
|
309
|
+
rep_data = json.loads(auto_repair_file.read_text(encoding="utf-8"))
|
|
310
|
+
latest_repair = rep_data.get("latest_summary")
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"success": True,
|
|
316
|
+
"tenant_id": tenant_id,
|
|
317
|
+
"total_services": len(enriched),
|
|
318
|
+
"validated_count": validated_count,
|
|
319
|
+
"validation_rate": round((validated_count / len(enriched)) * 100, 1) if enriched else 100.0,
|
|
320
|
+
"active_sessions_count": active_sessions,
|
|
321
|
+
"pending_hitl_count": len(pending_tasks),
|
|
322
|
+
"latest_auto_repair": latest_repair,
|
|
323
|
+
"services": enriched,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.post("/api/v1/onboard")
|
|
328
|
+
async def onboard_site_endpoint(req: OnboardRequest) -> Any:
|
|
329
|
+
"""
|
|
330
|
+
Web Dashboard üzerinden yeni bir siteyi sisteme ekler (`onboard_new_site`):
|
|
331
|
+
1. Paket bazlı maksimum site limitini (`max_sites` / `active_sites`) kontrol eder.
|
|
332
|
+
2. Plaid tarzı yasal onay (User Delegation Consent) kontrolünü yapar ve SHA-256 imzasıyla loglar.
|
|
333
|
+
3. Tarayıcı oturumunu açıp XHR/Fetch trafiğini yakalar (`wait_seconds` veya tarayıcı içi 'Kaydı Bitir' butonuyla).
|
|
334
|
+
4. GPT-4o ile `client.py`, `schema.json` ve `__init__.py` dosyalarını derler.
|
|
335
|
+
5. 3x Auto-Test (Compiler + Validator Loop) doğrulamasını çalıştırıp sonucu döndürür.
|
|
336
|
+
"""
|
|
337
|
+
if not req.consent_accepted:
|
|
338
|
+
raise HTTPException(
|
|
339
|
+
status_code=400,
|
|
340
|
+
detail="Plaid tarzı Yasal Onay (User Delegation Disclaimer) kutucuğu onaylanmalıdır.",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
svc_name = req.service_name.strip()
|
|
344
|
+
target_url = req.target_url.strip()
|
|
345
|
+
if not svc_name or not target_url:
|
|
346
|
+
raise HTTPException(status_code=400, detail="Site adı (service_name) ve Hedef URL (target_url) zorunludur.")
|
|
347
|
+
|
|
348
|
+
if not target_url.startswith(("http://", "https://")):
|
|
349
|
+
target_url = f"https://{target_url}"
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
register_site_usage(req.tenant_id, svc_name)
|
|
353
|
+
except SiteLimitExceededError as exc:
|
|
354
|
+
acc = _build_account_payload(tenant_id=req.tenant_id)
|
|
355
|
+
return JSONResponse(
|
|
356
|
+
status_code=403,
|
|
357
|
+
content={
|
|
358
|
+
"success": False,
|
|
359
|
+
"service_name": svc_name,
|
|
360
|
+
"target_url": target_url,
|
|
361
|
+
"tenant_id": acc["tenant_id"],
|
|
362
|
+
"plan": acc["plan"],
|
|
363
|
+
"max_sites": acc["max_sites"],
|
|
364
|
+
"active_sites": acc["active_sites"],
|
|
365
|
+
"active_sites_count": acc["active_sites_count"],
|
|
366
|
+
"error": str(exc),
|
|
367
|
+
"detail": str(exc),
|
|
368
|
+
"warning": str(exc),
|
|
369
|
+
"message": str(exc),
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
start_t = time.perf_counter()
|
|
374
|
+
try:
|
|
375
|
+
result = await asyncio.to_thread(
|
|
376
|
+
onboard_new_site,
|
|
377
|
+
service_name=svc_name,
|
|
378
|
+
start_url=target_url,
|
|
379
|
+
description=req.description.strip(),
|
|
380
|
+
headless=(req.mode.strip().lower() == "cloud"),
|
|
381
|
+
tenant_id=req.tenant_id,
|
|
382
|
+
mode=req.mode,
|
|
383
|
+
auto_validate=True,
|
|
384
|
+
validation_runs=req.validation_runs,
|
|
385
|
+
interactive_tos=False,
|
|
386
|
+
wait_seconds=req.wait_seconds,
|
|
387
|
+
)
|
|
388
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
389
|
+
acc = _build_account_payload(tenant_id=req.tenant_id)
|
|
390
|
+
return {
|
|
391
|
+
**result,
|
|
392
|
+
"max_sites": acc["max_sites"],
|
|
393
|
+
"active_sites": acc["active_sites"],
|
|
394
|
+
"active_sites_count": acc["active_sites_count"],
|
|
395
|
+
"elapsed_ms": elapsed_ms,
|
|
396
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
397
|
+
}
|
|
398
|
+
except Exception as exc:
|
|
399
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
400
|
+
return {
|
|
401
|
+
"success": False,
|
|
402
|
+
"service_name": svc_name,
|
|
403
|
+
"target_url": target_url,
|
|
404
|
+
"elapsed_ms": elapsed_ms,
|
|
405
|
+
"error": str(exc),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _detect_service_from_prompt(prompt: str, explicit_service: str | None = None) -> str | None:
|
|
410
|
+
"""Doğal dil sorusu (`prompt`) içinden kullanılan site/konektör adını tespit eder."""
|
|
411
|
+
if explicit_service and explicit_service.strip():
|
|
412
|
+
return explicit_service.strip().lower()
|
|
413
|
+
lower_p = (prompt or "").lower()
|
|
414
|
+
reg_dir = PROJECT_ROOT / "registry"
|
|
415
|
+
candidates: list[str] = []
|
|
416
|
+
if reg_dir.exists():
|
|
417
|
+
for child in sorted(reg_dir.iterdir()):
|
|
418
|
+
if child.is_dir() and not child.name.startswith("_"):
|
|
419
|
+
candidates.append(child.name.lower())
|
|
420
|
+
for extra in ("thy", "trendyol", "pegasus", "enuygun", "wikipedia", "linkedin_jobs", "biletix", "obilet", "hepsiburada", "amazon"):
|
|
421
|
+
if extra not in candidates:
|
|
422
|
+
candidates.append(extra)
|
|
423
|
+
if "türk hava yolları" in lower_p or "turk hava yollari" in lower_p or "turkish airlines" in lower_p:
|
|
424
|
+
return "thy"
|
|
425
|
+
if "linkedin" in lower_p and "linkedin_jobs" in candidates:
|
|
426
|
+
return "linkedin_jobs"
|
|
427
|
+
for svc in candidates:
|
|
428
|
+
if svc in lower_p:
|
|
429
|
+
return svc
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _consume_and_resolve_account(
|
|
434
|
+
tenant_id: str = "default",
|
|
435
|
+
api_key: str | None = None,
|
|
436
|
+
requested_mode: str | None = None,
|
|
437
|
+
service_name: str | None = None,
|
|
438
|
+
) -> dict[str, Any]:
|
|
439
|
+
"""
|
|
440
|
+
`X-API-Key` / `api_key` veya `tenant_id` üzerinden kullanıcıyı bulur,
|
|
441
|
+
varsa `service_name` için paket bazlı site limitini (`register_site_usage`) kontrol eder,
|
|
442
|
+
`core/api_keys.py` (`consume_credit`) ile 1 kredi düşer ve aktif modu ('local' / 'cloud')
|
|
443
|
+
ile güncel kredi/paket/site bilgilerini döndürür.
|
|
444
|
+
"""
|
|
445
|
+
auth_id = (api_key or tenant_id or "default").strip() or "default"
|
|
446
|
+
info_before = get_tenant_info(auth_id)
|
|
447
|
+
resolved_tenant = str(info_before.get("tenant_id") or tenant_id or "default")
|
|
448
|
+
if service_name:
|
|
449
|
+
register_site_usage(auth_id, service_name)
|
|
450
|
+
consume_credit(auth_id, amount=1, service=service_name)
|
|
451
|
+
acc = _build_account_payload(tenant_id=resolved_tenant)
|
|
452
|
+
|
|
453
|
+
plan_mode = str(acc.get("mode", "local")).strip().lower()
|
|
454
|
+
req_mode_clean = str(requested_mode or "").strip().lower()
|
|
455
|
+
if plan_mode == "cloud" or req_mode_clean == "cloud":
|
|
456
|
+
active_mode = "cloud"
|
|
457
|
+
elif req_mode_clean in ("local", "cloud"):
|
|
458
|
+
active_mode = req_mode_clean
|
|
459
|
+
else:
|
|
460
|
+
active_mode = plan_mode if plan_mode in ("local", "cloud") else "local"
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
"tenant_id": resolved_tenant,
|
|
464
|
+
"api_key": acc.get("api_key"),
|
|
465
|
+
"plan": acc.get("plan", "free"),
|
|
466
|
+
"plan_label": acc.get("plan_label"),
|
|
467
|
+
"mode": active_mode,
|
|
468
|
+
"remaining_credits": int(acc.get("remaining_credits", 0)),
|
|
469
|
+
"used_credits": int(acc.get("used_credits", 0)),
|
|
470
|
+
"credits": int(acc.get("credits", 0)),
|
|
471
|
+
"max_credits": int(acc.get("max_credits", 1000)),
|
|
472
|
+
"max_sites": int(acc.get("max_sites", 2)),
|
|
473
|
+
"active_sites": list(acc.get("active_sites", [])),
|
|
474
|
+
"active_sites_count": int(acc.get("active_sites_count", 0)),
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@app.post("/api/v1/call")
|
|
479
|
+
async def call_service(
|
|
480
|
+
req: CallRequest,
|
|
481
|
+
x_api_key: str | None = Header(default=None, alias="X-API-Key"),
|
|
482
|
+
) -> Any:
|
|
483
|
+
"""
|
|
484
|
+
İlgili servisi User-Delegated Browser Middleware üzerinden çalıştırıp JSON yanıtını,
|
|
485
|
+
süresini (ms) ve güncel kredi/paket/mod/site limiti durumunu döndürür.
|
|
486
|
+
"""
|
|
487
|
+
try:
|
|
488
|
+
credit_ctx = _consume_and_resolve_account(
|
|
489
|
+
tenant_id=req.tenant_id,
|
|
490
|
+
api_key=x_api_key or req.api_key,
|
|
491
|
+
requested_mode=req.mode,
|
|
492
|
+
service_name=req.service,
|
|
493
|
+
)
|
|
494
|
+
except SiteLimitExceededError as exc:
|
|
495
|
+
auth_id = (x_api_key or req.api_key or req.tenant_id or "default").strip() or "default"
|
|
496
|
+
acc = _build_account_payload(tenant_id=str(get_tenant_info(auth_id).get("tenant_id", req.tenant_id)))
|
|
497
|
+
return JSONResponse(
|
|
498
|
+
status_code=403,
|
|
499
|
+
content={
|
|
500
|
+
"success": False,
|
|
501
|
+
"service": req.service,
|
|
502
|
+
"tenant_id": acc["tenant_id"],
|
|
503
|
+
"plan": acc["plan"],
|
|
504
|
+
"mode": acc["mode"],
|
|
505
|
+
"remaining_credits": acc["remaining_credits"],
|
|
506
|
+
"used_credits": acc["used_credits"],
|
|
507
|
+
"max_sites": acc["max_sites"],
|
|
508
|
+
"active_sites": acc["active_sites"],
|
|
509
|
+
"active_sites_count": acc["active_sites_count"],
|
|
510
|
+
"error": str(exc),
|
|
511
|
+
"detail": str(exc),
|
|
512
|
+
"warning": str(exc),
|
|
513
|
+
"message": str(exc),
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
except ValueError as exc:
|
|
517
|
+
raise HTTPException(status_code=402, detail=str(exc))
|
|
518
|
+
|
|
519
|
+
active_tenant = credit_ctx["tenant_id"]
|
|
520
|
+
active_mode = credit_ctx["mode"]
|
|
521
|
+
|
|
522
|
+
client = OwnAPIClient(
|
|
523
|
+
api_key=x_api_key or req.api_key,
|
|
524
|
+
tenant_id=active_tenant,
|
|
525
|
+
mode=active_mode,
|
|
526
|
+
non_interactive=req.non_interactive,
|
|
527
|
+
)
|
|
528
|
+
start_t = time.perf_counter()
|
|
529
|
+
try:
|
|
530
|
+
result = await asyncio.to_thread(
|
|
531
|
+
client.call,
|
|
532
|
+
req.service,
|
|
533
|
+
mode=active_mode,
|
|
534
|
+
non_interactive=req.non_interactive,
|
|
535
|
+
_consume_credit=False,
|
|
536
|
+
**req.params,
|
|
537
|
+
)
|
|
538
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
539
|
+
return {
|
|
540
|
+
"success": True,
|
|
541
|
+
"service": req.service,
|
|
542
|
+
"tenant_id": active_tenant,
|
|
543
|
+
"plan": credit_ctx["plan"],
|
|
544
|
+
"mode": active_mode,
|
|
545
|
+
"remaining_credits": credit_ctx["remaining_credits"],
|
|
546
|
+
"used_credits": credit_ctx["used_credits"],
|
|
547
|
+
"credits": credit_ctx["credits"],
|
|
548
|
+
"max_credits": credit_ctx["max_credits"],
|
|
549
|
+
"max_sites": credit_ctx["max_sites"],
|
|
550
|
+
"active_sites": credit_ctx["active_sites"],
|
|
551
|
+
"active_sites_count": credit_ctx["active_sites_count"],
|
|
552
|
+
"non_interactive": req.non_interactive,
|
|
553
|
+
"elapsed_ms": elapsed_ms,
|
|
554
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
555
|
+
"data": result,
|
|
556
|
+
}
|
|
557
|
+
except ValueError as exc:
|
|
558
|
+
raise HTTPException(status_code=404, detail=str(exc))
|
|
559
|
+
except Exception as exc:
|
|
560
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
561
|
+
return {
|
|
562
|
+
"success": False,
|
|
563
|
+
"service": req.service,
|
|
564
|
+
"tenant_id": active_tenant,
|
|
565
|
+
"plan": credit_ctx["plan"],
|
|
566
|
+
"mode": active_mode,
|
|
567
|
+
"remaining_credits": credit_ctx["remaining_credits"],
|
|
568
|
+
"used_credits": credit_ctx["used_credits"],
|
|
569
|
+
"credits": credit_ctx["credits"],
|
|
570
|
+
"max_credits": credit_ctx["max_credits"],
|
|
571
|
+
"max_sites": credit_ctx["max_sites"],
|
|
572
|
+
"active_sites": credit_ctx["active_sites"],
|
|
573
|
+
"active_sites_count": credit_ctx["active_sites_count"],
|
|
574
|
+
"elapsed_ms": elapsed_ms,
|
|
575
|
+
"error": str(exc),
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@app.post("/api/v1/ask")
|
|
580
|
+
async def ask_agent(
|
|
581
|
+
req: AskRequest,
|
|
582
|
+
x_api_key: str | None = Header(default=None, alias="X-API-Key"),
|
|
583
|
+
) -> Any:
|
|
584
|
+
"""
|
|
585
|
+
Doğal dil ajanını (`OwnAPIClient.ask`) çalıştırıp yanıtı ve güncel kredi/paket/mod/site limiti durumunu döndürür.
|
|
586
|
+
"""
|
|
587
|
+
detected_svc = _detect_service_from_prompt(req.prompt, req.service)
|
|
588
|
+
try:
|
|
589
|
+
credit_ctx = _consume_and_resolve_account(
|
|
590
|
+
tenant_id=req.tenant_id,
|
|
591
|
+
api_key=x_api_key or req.api_key,
|
|
592
|
+
requested_mode=req.mode,
|
|
593
|
+
service_name=detected_svc,
|
|
594
|
+
)
|
|
595
|
+
except SiteLimitExceededError as exc:
|
|
596
|
+
auth_id = (x_api_key or req.api_key or req.tenant_id or "default").strip() or "default"
|
|
597
|
+
acc = _build_account_payload(tenant_id=str(get_tenant_info(auth_id).get("tenant_id", req.tenant_id)))
|
|
598
|
+
return JSONResponse(
|
|
599
|
+
status_code=403,
|
|
600
|
+
content={
|
|
601
|
+
"success": False,
|
|
602
|
+
"prompt": req.prompt,
|
|
603
|
+
"service": detected_svc,
|
|
604
|
+
"tenant_id": acc["tenant_id"],
|
|
605
|
+
"plan": acc["plan"],
|
|
606
|
+
"mode": acc["mode"],
|
|
607
|
+
"remaining_credits": acc["remaining_credits"],
|
|
608
|
+
"used_credits": acc["used_credits"],
|
|
609
|
+
"max_sites": acc["max_sites"],
|
|
610
|
+
"active_sites": acc["active_sites"],
|
|
611
|
+
"active_sites_count": acc["active_sites_count"],
|
|
612
|
+
"error": str(exc),
|
|
613
|
+
"detail": str(exc),
|
|
614
|
+
"warning": str(exc),
|
|
615
|
+
"message": str(exc),
|
|
616
|
+
},
|
|
617
|
+
)
|
|
618
|
+
except ValueError as exc:
|
|
619
|
+
raise HTTPException(status_code=402, detail=str(exc))
|
|
620
|
+
|
|
621
|
+
active_tenant = credit_ctx["tenant_id"]
|
|
622
|
+
active_mode = credit_ctx["mode"]
|
|
623
|
+
|
|
624
|
+
client = OwnAPIClient(
|
|
625
|
+
api_key=x_api_key or req.api_key,
|
|
626
|
+
tenant_id=active_tenant,
|
|
627
|
+
mode=active_mode,
|
|
628
|
+
)
|
|
629
|
+
start_t = time.perf_counter()
|
|
630
|
+
try:
|
|
631
|
+
answer = await asyncio.to_thread(client.ask, req.prompt, _consume_credit=False)
|
|
632
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
633
|
+
return {
|
|
634
|
+
"success": True,
|
|
635
|
+
"prompt": req.prompt,
|
|
636
|
+
"service": detected_svc,
|
|
637
|
+
"tenant_id": active_tenant,
|
|
638
|
+
"plan": credit_ctx["plan"],
|
|
639
|
+
"mode": active_mode,
|
|
640
|
+
"remaining_credits": credit_ctx["remaining_credits"],
|
|
641
|
+
"used_credits": credit_ctx["used_credits"],
|
|
642
|
+
"credits": credit_ctx["credits"],
|
|
643
|
+
"max_credits": credit_ctx["max_credits"],
|
|
644
|
+
"max_sites": credit_ctx["max_sites"],
|
|
645
|
+
"active_sites": credit_ctx["active_sites"],
|
|
646
|
+
"active_sites_count": credit_ctx["active_sites_count"],
|
|
647
|
+
"elapsed_ms": elapsed_ms,
|
|
648
|
+
"answer": answer,
|
|
649
|
+
}
|
|
650
|
+
except Exception as exc:
|
|
651
|
+
elapsed_ms = round((time.perf_counter() - start_t) * 1000.0, 2)
|
|
652
|
+
return {
|
|
653
|
+
"success": False,
|
|
654
|
+
"prompt": req.prompt,
|
|
655
|
+
"service": detected_svc,
|
|
656
|
+
"tenant_id": active_tenant,
|
|
657
|
+
"plan": credit_ctx["plan"],
|
|
658
|
+
"mode": active_mode,
|
|
659
|
+
"remaining_credits": credit_ctx["remaining_credits"],
|
|
660
|
+
"used_credits": credit_ctx["used_credits"],
|
|
661
|
+
"credits": credit_ctx["credits"],
|
|
662
|
+
"max_credits": credit_ctx["max_credits"],
|
|
663
|
+
"max_sites": credit_ctx["max_sites"],
|
|
664
|
+
"active_sites": credit_ctx["active_sites"],
|
|
665
|
+
"active_sites_count": credit_ctx["active_sites_count"],
|
|
666
|
+
"elapsed_ms": elapsed_ms,
|
|
667
|
+
"error": str(exc),
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
@app.post("/api/v1/validate/{service_name}")
|
|
672
|
+
async def validate_single_service(
|
|
673
|
+
service_name: str,
|
|
674
|
+
req: ValidateRequest | None = None,
|
|
675
|
+
) -> dict[str, Any]:
|
|
676
|
+
"""Tek bir servis için 3x Auto-Test (Compiler + Validator Loop) doğrulamasını çalıştırır."""
|
|
677
|
+
cfg = req or ValidateRequest()
|
|
678
|
+
service_dir = PROJECT_ROOT / "registry" / service_name
|
|
679
|
+
client_path = service_dir / "client.py"
|
|
680
|
+
schema_path = service_dir / "schema.json"
|
|
681
|
+
traffic_path = service_dir / "captured_traffic.json"
|
|
682
|
+
|
|
683
|
+
if not client_path.exists() or not schema_path.exists():
|
|
684
|
+
raise HTTPException(status_code=404, detail=f"Servis '{service_name}' bulunamadı.")
|
|
685
|
+
|
|
686
|
+
raw_schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
687
|
+
schema_obj = RegistrySchema.model_validate(raw_schema)
|
|
688
|
+
endpoints: list[dict[str, Any]] = []
|
|
689
|
+
if traffic_path.exists():
|
|
690
|
+
try:
|
|
691
|
+
tdata = json.loads(traffic_path.read_text(encoding="utf-8"))
|
|
692
|
+
endpoints = tdata.get("endpoints", [])
|
|
693
|
+
except Exception:
|
|
694
|
+
endpoints = []
|
|
695
|
+
|
|
696
|
+
matched_ep = next(
|
|
697
|
+
(e for e in endpoints if e.get("url") == schema_obj.endpoint_url),
|
|
698
|
+
endpoints[0] if endpoints else {},
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
report = await asyncio.to_thread(
|
|
702
|
+
validate_and_repair_client,
|
|
703
|
+
service_name=service_name,
|
|
704
|
+
target_url=schema_obj.target_url,
|
|
705
|
+
description=schema_obj.goal,
|
|
706
|
+
client_path=client_path,
|
|
707
|
+
schema_obj=schema_obj,
|
|
708
|
+
matched_endpoint=matched_ep,
|
|
709
|
+
all_endpoints=endpoints,
|
|
710
|
+
validation_runs=cfg.runs,
|
|
711
|
+
max_repair_attempts=3,
|
|
712
|
+
tenant_id=cfg.tenant_id,
|
|
713
|
+
mode=cfg.mode,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
raw_schema["validation_status"] = report.get("validation_status", "BAŞARILI")
|
|
717
|
+
raw_schema["validation_passed_runs"] = report.get("passed_runs", cfg.runs)
|
|
718
|
+
raw_schema["last_validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
719
|
+
schema_path.write_text(json.dumps(raw_schema, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
"success": report.get("validation_status") == "BAŞARILI",
|
|
723
|
+
"service_name": service_name,
|
|
724
|
+
**report,
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@app.post("/api/v1/auto-repair")
|
|
729
|
+
async def run_auto_repair(req: AutoRepairRequest | None = None) -> dict[str, Any]:
|
|
730
|
+
"""
|
|
731
|
+
`AutoRepairEngine.validate_all_services(runs=1, auto_heal=True)` tetikleyip
|
|
732
|
+
toplu sağlık ve onarım raporunu (`.sessions/auto_repair_report.json`) döndürür.
|
|
733
|
+
"""
|
|
734
|
+
cfg = req or AutoRepairRequest()
|
|
735
|
+
engine = AutoRepairEngine(tenant_id=cfg.tenant_id, mode=cfg.mode)
|
|
736
|
+
report = await asyncio.to_thread(
|
|
737
|
+
engine.validate_all_services,
|
|
738
|
+
runs=cfg.runs,
|
|
739
|
+
auto_heal=cfg.auto_heal,
|
|
740
|
+
services=cfg.services,
|
|
741
|
+
)
|
|
742
|
+
return {
|
|
743
|
+
"success": True,
|
|
744
|
+
**report,
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@app.get("/api/v1/auto-repair/report")
|
|
749
|
+
async def get_auto_repair_report() -> dict[str, Any]:
|
|
750
|
+
"""Kaydedilmiş son `.sessions/auto_repair_report.json` geçmişini döndürür."""
|
|
751
|
+
report_file = PROJECT_ROOT / ".sessions" / "auto_repair_report.json"
|
|
752
|
+
if not report_file.exists():
|
|
753
|
+
return {"success": True, "total_runs": 0, "latest_summary": None, "history": []}
|
|
754
|
+
try:
|
|
755
|
+
data = json.loads(report_file.read_text(encoding="utf-8"))
|
|
756
|
+
return {"success": True, **data}
|
|
757
|
+
except Exception as exc:
|
|
758
|
+
return {"success": False, "error": str(exc), "history": []}
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@app.get("/api/hitl/pending")
|
|
762
|
+
@app.get("/api/v1/hitl/pending")
|
|
763
|
+
async def get_hitl_pending(
|
|
764
|
+
tenant_id: str | None = Query(default=None, description="Kiracı filtresi (boş ise tüm kiracılar)"),
|
|
765
|
+
) -> dict[str, Any]:
|
|
766
|
+
"""
|
|
767
|
+
Proje kök dizinindeki `.sessions/hitl_state.json` dosyasından bekleyen (`PAUSED` / `PENDING_USER_ACTION`)
|
|
768
|
+
HITL onay isteklerini diskten okuyarak döndürür. Sayfa yenilendiğinde (F5) veya 2 saniyelik
|
|
769
|
+
polling sırasında bekleyen onayların kaybolmadan anında ekrana gelmesini sağlar.
|
|
770
|
+
"""
|
|
771
|
+
mgr = HITLQueueManager()
|
|
772
|
+
all_tasks = mgr.list_tasks(tenant_id=tenant_id, status_filter=None)
|
|
773
|
+
pending_tasks = [t for t in all_tasks if mgr.is_pending_status(t.get("status"))]
|
|
774
|
+
completed_tasks = [t for t in all_tasks if str(t.get("status", "")).upper() == "COMPLETED"]
|
|
775
|
+
return {
|
|
776
|
+
"success": True,
|
|
777
|
+
"state_file": ".sessions/hitl_state.json",
|
|
778
|
+
"total_tasks": len(all_tasks),
|
|
779
|
+
"pending_count": len(pending_tasks),
|
|
780
|
+
"paused_count": len(pending_tasks),
|
|
781
|
+
"completed_count": len(completed_tasks),
|
|
782
|
+
"pending_tasks": list(reversed(pending_tasks)),
|
|
783
|
+
"tasks": list(reversed(all_tasks)),
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
@app.get("/api/hitl/queue")
|
|
788
|
+
@app.get("/api/v1/hitl/queue")
|
|
789
|
+
async def get_hitl_queue(
|
|
790
|
+
tenant_id: str | None = Query(default=None, description="Kiracı filtresi (boş ise tüm kiracılar)"),
|
|
791
|
+
status: str | None = Query(default=None, description="PAUSED, PENDING_USER_ACTION veya COMPLETED"),
|
|
792
|
+
) -> dict[str, Any]:
|
|
793
|
+
"""`HITLQueueManager` (`.sessions/hitl_state.json`) üzerinden bekleyen (`PAUSED`) ve tamamlanan görevleri listeler."""
|
|
794
|
+
mgr = HITLQueueManager()
|
|
795
|
+
tasks = mgr.list_tasks(tenant_id=tenant_id, status_filter=status)
|
|
796
|
+
pending_count = sum(1 for t in tasks if mgr.is_pending_status(t.get("status")))
|
|
797
|
+
completed_count = sum(1 for t in tasks if str(t.get("status", "")).upper() == "COMPLETED")
|
|
798
|
+
return {
|
|
799
|
+
"success": True,
|
|
800
|
+
"state_file": ".sessions/hitl_state.json",
|
|
801
|
+
"total_tasks": len(tasks),
|
|
802
|
+
"pending_count": pending_count,
|
|
803
|
+
"paused_count": pending_count,
|
|
804
|
+
"completed_count": completed_count,
|
|
805
|
+
"tasks": list(reversed(tasks)),
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@app.get("/api/hitl/preview/{task_id}")
|
|
810
|
+
@app.get("/api/v1/hitl/preview/{task_id}")
|
|
811
|
+
async def get_hitl_preview(
|
|
812
|
+
task_id: str,
|
|
813
|
+
tenant_id: str | None = Query(default=None, description="İsteğe bağlı kiracı filtresi"),
|
|
814
|
+
) -> dict[str, Any]:
|
|
815
|
+
"""
|
|
816
|
+
Aktif veya duraklatılmış (`PAUSED`) HITL görevi için bulut tarayıcı ekran görüntüsünü (`screenshot_b64`)
|
|
817
|
+
ve canlı durum önizlemesini döndürür.
|
|
818
|
+
"""
|
|
819
|
+
mgr = HITLQueueManager()
|
|
820
|
+
preview = mgr.get_task_preview(task_id=task_id, tenant_id=tenant_id)
|
|
821
|
+
if not preview:
|
|
822
|
+
raise HTTPException(status_code=404, detail=f"HITL görevi '{task_id}' bulunamadı.")
|
|
823
|
+
return preview
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@app.post("/api/hitl/resolve/{task_id}")
|
|
827
|
+
@app.post("/api/v1/hitl/resolve/{task_id}")
|
|
828
|
+
@app.post("/api/hitl/approve/{task_id}")
|
|
829
|
+
@app.post("/api/hitl/resume/{task_id}")
|
|
830
|
+
@app.post("/api/v1/hitl/approve/{task_id}")
|
|
831
|
+
@app.post("/api/v1/hitl/resume/{task_id}")
|
|
832
|
+
async def approve_hitl_task(
|
|
833
|
+
task_id: str,
|
|
834
|
+
req: HITLResolveRequest | None = Body(default=None),
|
|
835
|
+
tenant_id: str | None = Query(default=None),
|
|
836
|
+
otp_code: str | None = Query(default=None, description="Opsiyonel SMS / E-Posta / 2FA doğrulama kodu"),
|
|
837
|
+
) -> dict[str, Any]:
|
|
838
|
+
"""
|
|
839
|
+
`.sessions/hitl_state.json` içindeki bekleyen (`PAUSED`) HITL görevini onaylar,
|
|
840
|
+
varsa `otp_code` (SMS/E-Posta doğrulama kodu) parametresini açık tarayıcıya iletir
|
|
841
|
+
ve duraklatılmış ajanın kaldığı yerden (`resume_task`) devam etmesini sağlar.
|
|
842
|
+
"""
|
|
843
|
+
eff_tenant = (req.tenant_id if req and req.tenant_id else tenant_id) or None
|
|
844
|
+
eff_otp = (req.otp_code if req and req.otp_code is not None else otp_code) or None
|
|
845
|
+
if eff_otp is not None:
|
|
846
|
+
eff_otp = str(eff_otp).strip() or None
|
|
847
|
+
|
|
848
|
+
mgr = HITLQueueManager()
|
|
849
|
+
task, _ = mgr.get_task(task_id, tenant_id=eff_tenant)
|
|
850
|
+
if not task:
|
|
851
|
+
raise HTTPException(status_code=404, detail=f"HITL görevi '{task_id}' bulunamadı.")
|
|
852
|
+
try:
|
|
853
|
+
res = await asyncio.to_thread(
|
|
854
|
+
mgr.resume_task,
|
|
855
|
+
task_id=task_id,
|
|
856
|
+
tenant_id=eff_tenant,
|
|
857
|
+
otp_code=eff_otp,
|
|
858
|
+
)
|
|
859
|
+
return {"success": True, **res}
|
|
860
|
+
except Exception as exc:
|
|
861
|
+
raise HTTPException(status_code=500, detail=str(exc))
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
@app.get("/api/v1/consent")
|
|
865
|
+
async def get_consent_audit(tenant_id: str = Query(default="default")) -> dict[str, Any]:
|
|
866
|
+
"""`ConsentManager` üzerinden SHA-256 imzalı yasal kullanıcı delegasyon kayıtlarını döndürür."""
|
|
867
|
+
mgr = ConsentManager()
|
|
868
|
+
records = mgr.get_consents(tenant_id=tenant_id)
|
|
869
|
+
return {
|
|
870
|
+
"success": True,
|
|
871
|
+
"tenant_id": tenant_id,
|
|
872
|
+
"tos_version": TOS_VERSION,
|
|
873
|
+
"disclaimer": USER_DELEGATION_DISCLAIMER,
|
|
874
|
+
"total_records": len(records),
|
|
875
|
+
"consents": list(reversed(records)),
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
if STATIC_DIR.exists():
|
|
880
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def run_server(host: str = "127.0.0.1", port: int = 8000, reload: bool = False) -> None:
|
|
884
|
+
"""FastAPI sunucusunu Uvicorn ile başlatır."""
|
|
885
|
+
import uvicorn
|
|
886
|
+
|
|
887
|
+
uvicorn.run("server:app" if reload else app, host=host, port=port, reload=reload)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
if __name__ == "__main__":
|
|
891
|
+
run_server()
|