daily 自建 Cursor bridge;DAILY_LLM_PROVIDER 控制后端;config/feeds.yaml 与 sensitive_words 可配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""Windows 兼容的 Cursor SDK bridge(daily 包自用,不依赖 bot/)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import codecs
|
||
import json
|
||
import logging
|
||
import os
|
||
import subprocess
|
||
import threading
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Any, Mapping
|
||
|
||
from daily.config import ROOT, env
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
READY_LINE_PREFIX = "cursor-sdk-bridge ready "
|
||
_bridge_lock = threading.Lock()
|
||
_bridge_process: subprocess.Popen[bytes] | None = None
|
||
|
||
|
||
def cursor_cwd() -> str:
|
||
return env("DAILY_CURSOR_CWD") or env("CURSOR_CWD") or str(ROOT)
|
||
|
||
|
||
def _parse_discovery_line(line: str) -> Mapping[str, Any] | None:
|
||
if not line.startswith(READY_LINE_PREFIX):
|
||
return None
|
||
payload = line[len(READY_LINE_PREFIX) :].strip()
|
||
loaded = json.loads(payload)
|
||
if not isinstance(loaded, dict):
|
||
raise RuntimeError("Bridge discovery payload must be an object")
|
||
return loaded
|
||
|
||
|
||
def _read_discovery_polling(process: subprocess.Popen[bytes], timeout: float = 60) -> Mapping[str, Any]:
|
||
if process.stderr is None:
|
||
raise RuntimeError("Bridge stderr unavailable")
|
||
|
||
fd = process.stderr.fileno()
|
||
was_blocking = os.get_blocking(fd)
|
||
os.set_blocking(fd, False)
|
||
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
||
pending = ""
|
||
stderr_lines: list[str] = []
|
||
deadline = time.monotonic() + timeout
|
||
|
||
try:
|
||
while time.monotonic() < deadline:
|
||
try:
|
||
chunk = os.read(fd, 8192)
|
||
except BlockingIOError:
|
||
chunk = b""
|
||
|
||
if chunk:
|
||
pending += decoder.decode(chunk)
|
||
while "\n" in pending:
|
||
line, pending = pending.split("\n", 1)
|
||
stderr_lines.append(line)
|
||
discovery = _parse_discovery_line(line)
|
||
if discovery is not None:
|
||
return discovery
|
||
else:
|
||
code = process.poll()
|
||
if code is not None:
|
||
pending += decoder.decode(b"", final=True)
|
||
if pending.strip():
|
||
stderr_lines.append(pending.strip())
|
||
joined = "\n".join(stderr_lines)[-2000:]
|
||
raise RuntimeError(
|
||
f"Bridge 启动失败 exit={code}: {joined or '无 stderr 输出'}"
|
||
)
|
||
time.sleep(0.05)
|
||
finally:
|
||
os.set_blocking(fd, was_blocking)
|
||
|
||
raise RuntimeError("等待 Cursor bridge 就绪超时")
|
||
|
||
|
||
def _auth_token_from_discovery(discovery: Mapping[str, Any]) -> str:
|
||
token = str(discovery.get("authToken") or "").strip()
|
||
if token:
|
||
return token
|
||
token_file = discovery.get("authTokenFile")
|
||
if token_file:
|
||
return Path(str(token_file)).read_text(encoding="utf-8").strip()
|
||
raise RuntimeError("Bridge discovery 缺少 auth token")
|
||
|
||
|
||
def warm_cursor_bridge(force: bool = False) -> None:
|
||
"""启动 cursor-sdk-bridge 并写入 CURSOR_SDK_BRIDGE_* 环境变量。"""
|
||
global _bridge_process
|
||
|
||
with _bridge_lock:
|
||
if (
|
||
not force
|
||
and _bridge_process is not None
|
||
and _bridge_process.poll() is None
|
||
and os.environ.get("CURSOR_SDK_BRIDGE_URL")
|
||
and os.environ.get("CURSOR_SDK_BRIDGE_TOKEN")
|
||
):
|
||
return
|
||
|
||
if _bridge_process is not None and _bridge_process.poll() is None:
|
||
_bridge_process.terminate()
|
||
try:
|
||
_bridge_process.wait(timeout=5)
|
||
except subprocess.TimeoutExpired:
|
||
_bridge_process.kill()
|
||
|
||
from cursor_sdk._vendor import resolve_bridge_path
|
||
|
||
cwd = cursor_cwd()
|
||
os.environ["CURSOR_CWD"] = cwd
|
||
argv = [resolve_bridge_path(), "--workspace", cwd]
|
||
logger.info("启动 Cursor bridge workspace=%s", cwd)
|
||
|
||
process = subprocess.Popen(
|
||
argv,
|
||
stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.PIPE,
|
||
)
|
||
try:
|
||
discovery = _read_discovery_polling(process)
|
||
except Exception:
|
||
process.kill()
|
||
process.wait(timeout=5)
|
||
raise
|
||
|
||
url = str(discovery.get("url") or "").strip()
|
||
if not url:
|
||
host = str(discovery.get("host") or "127.0.0.1")
|
||
port = discovery.get("port")
|
||
url = f"http://{host}:{port}"
|
||
|
||
token = _auth_token_from_discovery(discovery)
|
||
os.environ["CURSOR_SDK_BRIDGE_URL"] = url
|
||
os.environ["CURSOR_SDK_BRIDGE_TOKEN"] = token
|
||
_bridge_process = process
|
||
logger.info("Cursor bridge 就绪: %s", url)
|