feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
"""Same-origin public BYOK gateway. Separate from the local Bearer application."""
|
||||
|
||||
import asyncio
|
||||
import hmac
|
||||
import ipaddress
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
from . import server
|
||||
from .model_catalog import ModelCatalog
|
||||
from .protocol import DecisionError, fields
|
||||
from .web_config import COOKIE, configure, public_config, website_origin
|
||||
from .web_sessions import Sessions, Visitor
|
||||
|
||||
VISITOR = web.RequestKey("website_visitor", Visitor)
|
||||
CLIENT_IP = web.RequestKey("website_ip", str)
|
||||
MANAGER = web.AppKey("website_sessions", Sessions)
|
||||
CATALOG = web.AppKey("website_catalog", ModelCatalog)
|
||||
PREFIX = server.PREFIX
|
||||
|
||||
|
||||
def view(visitor):
|
||||
s = visitor.service
|
||||
return {
|
||||
"version": "lekiwi-agent-v1",
|
||||
"configVersion": s.epoch,
|
||||
"configuration": public_config(s.connections.values),
|
||||
"ready": bool(s.connections.values),
|
||||
"active": len(s.active),
|
||||
"keyStorage": "memory-only",
|
||||
}
|
||||
|
||||
|
||||
def create_website_app(
|
||||
origin, directory=None, *, development=False, limits=None, trusted_proxies=()
|
||||
):
|
||||
host = website_origin(origin, development=development)
|
||||
manager = Sessions(directory or Path("/tmp/cadworld-sessions"), origin, limits)
|
||||
catalog = ModelCatalog()
|
||||
cookie = "cadworld-dev-session" if development else COOKIE
|
||||
trusted = set(trusted_proxies)
|
||||
|
||||
def client_ip(request):
|
||||
remote = request.remote or "unknown"
|
||||
if remote in trusted:
|
||||
try:
|
||||
return str(ipaddress.ip_address(request.headers.get("X-Real-IP", "")))
|
||||
except ValueError:
|
||||
raise DecisionError("invalid_proxy_ip", 403) from None
|
||||
return remote
|
||||
|
||||
@web.middleware
|
||||
async def boundary(request, handler):
|
||||
visitor = None
|
||||
try:
|
||||
if request.headers.get("Host") != host:
|
||||
raise DecisionError("host_forbidden", 403)
|
||||
supplied_origin = request.headers.get("Origin")
|
||||
if supplied_origin and supplied_origin != origin:
|
||||
raise DecisionError("origin_forbidden", 403)
|
||||
if request.headers.get("Sec-Fetch-Site") in ("cross-site", "same-site"):
|
||||
raise DecisionError("origin_forbidden", 403)
|
||||
if request.query_string:
|
||||
raise DecisionError("query_forbidden", 400)
|
||||
if request.path == "/healthz" and request.method == "GET":
|
||||
response = web.json_response({"ok": True})
|
||||
else:
|
||||
await manager.expire()
|
||||
write = request.method not in ("GET", "HEAD")
|
||||
if write and supplied_origin != origin:
|
||||
raise DecisionError("origin_required", 403)
|
||||
bootstrap = request.path == PREFIX + "/session" and request.method == "POST"
|
||||
visitor = manager.values.get(request.cookies.get(cookie, ""))
|
||||
if bootstrap:
|
||||
fields(await server.body(request), [])
|
||||
# Restoring a cookie is read-only: don't extend credential lifetime.
|
||||
if visitor is None:
|
||||
visitor = manager.create(client_ip(request))
|
||||
response = web.json_response({**view(visitor), "csrfToken": visitor.csrf})
|
||||
response.set_cookie(
|
||||
cookie,
|
||||
visitor.ident,
|
||||
secure=not development,
|
||||
httponly=True,
|
||||
samesite="Strict",
|
||||
path="/",
|
||||
)
|
||||
else:
|
||||
if visitor is None or visitor.closed:
|
||||
raise DecisionError("session_expired", 401)
|
||||
if write:
|
||||
if not hmac.compare_digest(
|
||||
request.headers.get("X-CSRF-Token", ""), visitor.csrf
|
||||
):
|
||||
raise DecisionError("csrf_required", 403)
|
||||
if request.path not in (PREFIX + "/session", PREFIX + "/cancel") and (
|
||||
request.headers.get("X-Config-Version") != str(visitor.service.epoch)
|
||||
):
|
||||
raise DecisionError("configuration_changed", 409)
|
||||
visitor.touched = time.monotonic()
|
||||
request[VISITOR] = visitor
|
||||
request[server.WEB_SERVICE] = visitor.service
|
||||
request[CLIENT_IP] = client_ip(request)
|
||||
response = await handler(request)
|
||||
except DecisionError as exc:
|
||||
response = web.json_response({"error": exc.code}, status=exc.status)
|
||||
except web.HTTPException as exc:
|
||||
response = web.json_response({"error": "http_request_rejected"}, status=exc.status)
|
||||
except Exception:
|
||||
response = web.json_response({"error": "internal_error"}, status=500)
|
||||
response.headers.update({"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"})
|
||||
if visitor and not visitor.closed:
|
||||
response.headers["X-Config-Version"] = str(visitor.service.epoch)
|
||||
return response
|
||||
|
||||
app = web.Application(middlewares=[boundary], client_max_size=65536)
|
||||
app[MANAGER], app[CATALOG] = manager, catalog
|
||||
|
||||
async def health(_):
|
||||
return web.json_response({"ok": True})
|
||||
|
||||
async def session(_):
|
||||
# Bootstrap handled in middleware, deliberately independent of model/CLI availability.
|
||||
raise DecisionError("invalid_session_method", 405)
|
||||
|
||||
async def destroy(request):
|
||||
await manager.destroy(request[VISITOR])
|
||||
response = web.json_response({"cleared": True})
|
||||
response.del_cookie(
|
||||
cookie, path="/", secure=not development, httponly=True, samesite="Strict"
|
||||
)
|
||||
return response
|
||||
|
||||
async def status(request):
|
||||
return web.json_response(view(request[VISITOR]))
|
||||
|
||||
async def models(_):
|
||||
await catalog.refresh(manager.http)
|
||||
return web.json_response(catalog.public())
|
||||
|
||||
async def configuration(request):
|
||||
visitor = request[VISITOR]
|
||||
s = visitor.service
|
||||
epoch = s.epoch
|
||||
data = await server.body(request)
|
||||
fields(data, ["llm", "jev"])
|
||||
fields(data["llm"], ["provider", "model"], ["apiKey"])
|
||||
codex_models = ()
|
||||
if data["llm"]["provider"] == "openrouter":
|
||||
await catalog.refresh(manager.http)
|
||||
elif data["llm"]["provider"] == "codex":
|
||||
if not visitor.codex_reserved or not (await s.codex.status())["loggedIn"]:
|
||||
raise DecisionError("codex_chatgpt_login_required", 409)
|
||||
codex_models = [m["id"] for m in (await s.codex.models())["models"]]
|
||||
if visitor.closed or s.epoch != epoch:
|
||||
raise DecisionError("configuration_changed", 409)
|
||||
values = configure(s.connections.values, data, catalog.models, codex_models)
|
||||
s.invalidate()
|
||||
s.connections.values = values
|
||||
return web.json_response(view(visitor))
|
||||
|
||||
async def inference(request):
|
||||
visitor = request[VISITOR]
|
||||
operation = request.match_info["operation"]
|
||||
llm = visitor.service.connections.values.get("llm")
|
||||
is_llm = operation == "plan" or (
|
||||
operation == "test" and (await server.body(request)).get("role") == "llm"
|
||||
)
|
||||
if is_llm and llm and llm.protocol == "codex" and not visitor.codex_reserved:
|
||||
raise DecisionError("codex_chatgpt_login_required", 409)
|
||||
manager.rate(request[CLIENT_IP], "calls", manager.limits.ip_calls)
|
||||
if manager.inference >= manager.limits.inference:
|
||||
raise DecisionError("server_busy", 429)
|
||||
manager.inference += 1
|
||||
try:
|
||||
return await {
|
||||
"plan": server.plan,
|
||||
"decide": server.decide,
|
||||
"test": server.test_connection,
|
||||
}[request.match_info["operation"]](request)
|
||||
finally:
|
||||
manager.inference -= 1
|
||||
|
||||
async def codex(request):
|
||||
visitor = request[VISITOR]
|
||||
s = visitor.service
|
||||
operation = request.match_info["operation"]
|
||||
if visitor.account_lock.locked():
|
||||
raise DecisionError("subscription_busy", 409)
|
||||
async with visitor.account_lock:
|
||||
if visitor.closed:
|
||||
raise DecisionError("session_expired", 401)
|
||||
if request.method == "POST":
|
||||
fields(await server.body(request), [])
|
||||
s.invalidate()
|
||||
if operation == "login":
|
||||
manager.rate(request[CLIENT_IP], "logins", manager.limits.ip_logins)
|
||||
manager.reserve_codex(visitor)
|
||||
try:
|
||||
value = await s.codex.login(device=True)
|
||||
visitor.login_deadline = time.monotonic() + 600
|
||||
except BaseException:
|
||||
await manager.close_codex(visitor)
|
||||
raise
|
||||
elif operation in ("cancel", "logout"):
|
||||
await manager.close_codex(visitor)
|
||||
value = {"loggedIn": False}
|
||||
elif not visitor.codex_reserved:
|
||||
value = {"loggedIn": False, "models": [], "planningAvailable": False}
|
||||
else:
|
||||
value = await {
|
||||
"status": s.codex.status,
|
||||
"models": s.codex.models,
|
||||
"limits": s.codex.limits,
|
||||
}[operation]()
|
||||
if operation == "status" and value.get("loggedIn"):
|
||||
visitor.login_deadline = 0
|
||||
return web.json_response(value)
|
||||
|
||||
app.router.add_get("/healthz", health)
|
||||
app.router.add_post(PREFIX + "/session", session)
|
||||
app.router.add_delete(PREFIX + "/session", destroy)
|
||||
app.router.add_get(PREFIX + "/status", status)
|
||||
app.router.add_get(PREFIX + "/models", models)
|
||||
app.router.add_put(PREFIX + "/configuration", configuration)
|
||||
app.router.add_post(PREFIX + "/{operation:plan|decide|test}", inference)
|
||||
app.router.add_post(PREFIX + "/cancel", server.cancel)
|
||||
app.router.add_get(PREFIX + "/codex/{operation:status|models|limits}", codex)
|
||||
app.router.add_post(PREFIX + "/codex/{operation:login|cancel|logout}", codex)
|
||||
|
||||
async def reap():
|
||||
while True:
|
||||
await asyncio.sleep(15)
|
||||
await manager.expire()
|
||||
|
||||
async def lifecycle(_):
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=False, connector=aiohttp.TCPConnector(limit=16)
|
||||
) as http:
|
||||
manager.http = http
|
||||
reaper = asyncio.create_task(reap())
|
||||
yield
|
||||
reaper.cancel()
|
||||
await asyncio.gather(reaper, return_exceptions=True)
|
||||
await manager.close()
|
||||
|
||||
app.cleanup_ctx.append(lifecycle)
|
||||
return app
|
||||
Reference in New Issue
Block a user