in which pylon replaces intercom (#2783)

This commit is contained in:
Shuchang Zheng
2025-06-25 05:45:07 +08:00
committed by GitHub
parent 6b5699a98c
commit 60dcd6bcb1
7 changed files with 91 additions and 4 deletions

View File

@@ -1,8 +1,13 @@
import { apiBaseUrl, artifactApiBaseUrl, envCredential } from "@/util/env"; import { apiBaseUrl, artifactApiBaseUrl, envCredential } from "@/util/env";
import axios from "axios"; import axios from "axios";
type ApiVersion = "sans-api-v1" | "v1" | "v2";
const apiV1BaseUrl = apiBaseUrl; const apiV1BaseUrl = apiBaseUrl;
const apiV2BaseUrl = apiBaseUrl.replace("v1", "v2"); const apiV2BaseUrl = apiBaseUrl.replace("v1", "v2");
const url = new URL(apiBaseUrl);
const pathname = url.pathname.replace("/api", "");
const apiSansApiV1BaseUrl = `${url.origin}${pathname}`;
const client = axios.create({ const client = axios.create({
baseURL: apiV1BaseUrl, baseURL: apiV1BaseUrl,
@@ -20,6 +25,14 @@ const v2Client = axios.create({
}, },
}); });
const clientSansApiV1 = axios.create({
baseURL: apiSansApiV1BaseUrl,
headers: {
"Content-Type": "application/json",
"x-api-key": envCredential,
},
});
const artifactApiClient = axios.create({ const artifactApiClient = axios.create({
baseURL: artifactApiBaseUrl, baseURL: artifactApiBaseUrl,
}); });
@@ -52,18 +65,36 @@ export function removeApiKeyHeader() {
async function getClient( async function getClient(
credentialGetter: CredentialGetter | null, credentialGetter: CredentialGetter | null,
version: string = "v1", version: ApiVersion = "v1",
) { ) {
const get = () => {
switch (version) {
case "sans-api-v1":
return clientSansApiV1;
case "v1":
return client;
case "v2":
return v2Client;
default: {
throw new Error(`Unknown version: ${version}`);
}
}
};
if (credentialGetter) { if (credentialGetter) {
removeApiKeyHeader(); removeApiKeyHeader();
const credential = await credentialGetter(); const credential = await credentialGetter();
if (!credential) { if (!credential) {
console.warn("No credential found"); console.warn("No credential found");
return version === "v1" ? client : v2Client; return get();
} }
setAuthorizationHeader(credential); setAuthorizationHeader(credential);
} }
return version === "v1" ? client : v2Client;
return get();
} }
export type CredentialGetter = () => Promise<string | null>; export type CredentialGetter = () => Promise<string | null>;

View File

@@ -411,3 +411,7 @@ export const RunEngine = {
} as const; } as const;
export type RunEngine = (typeof RunEngine)[keyof typeof RunEngine]; export type RunEngine = (typeof RunEngine)[keyof typeof RunEngine];
export type PylonEmailHash = {
hash: string;
};

7
skyvern-frontend/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface Window {
pylon: {
chat_settings: { [k: string]: string };
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Pylon: (method: string, ...args: any[]) => void;
}

View File

@@ -275,10 +275,15 @@ class Settings(BaseSettings):
SKYVERN_BROWSER_VNC_PORT: int = 6080 SKYVERN_BROWSER_VNC_PORT: int = 6080
""" """
The websockified port on which the VNC server of a persistent browser is The websockified port on which the VNC server of a persistent browser is
listening. listening.
""" """
PYLON_IDENTITY_VERIFICATION_SECRET: str | None = None
"""
The secret used to sign the email/identity of the user.
"""
def get_model_name_to_llm_key(self) -> dict[str, dict[str, str]]: def get_model_name_to_llm_key(self) -> dict[str, dict[str, str]]:
""" """
Keys are model names available to blocks in the frontend. These map to key names Keys are model names available to blocks in the frontend. These map to key names

View File

@@ -1,6 +1,7 @@
from skyvern.forge.sdk.routes import agent_protocol # noqa: F401 from skyvern.forge.sdk.routes import agent_protocol # noqa: F401
from skyvern.forge.sdk.routes import browser_sessions # noqa: F401 from skyvern.forge.sdk.routes import browser_sessions # noqa: F401
from skyvern.forge.sdk.routes import credentials # noqa: F401 from skyvern.forge.sdk.routes import credentials # noqa: F401
from skyvern.forge.sdk.routes import pylon # noqa: F401
from skyvern.forge.sdk.routes import streaming # noqa: F401 from skyvern.forge.sdk.routes import streaming # noqa: F401
from skyvern.forge.sdk.routes import streaming_commands # noqa: F401 from skyvern.forge.sdk.routes import streaming_commands # noqa: F401
from skyvern.forge.sdk.routes import streaming_vnc # noqa: F401 from skyvern.forge.sdk.routes import streaming_vnc # noqa: F401

View File

@@ -0,0 +1,34 @@
import hashlib
import hmac
import structlog
from fastapi import Query
from skyvern.config import settings
from skyvern.forge.sdk.routes.routers import base_router
from skyvern.forge.sdk.schemas.pylon import PylonHash
LOG = structlog.get_logger()
@base_router.get(
"/pylon/email_hash",
include_in_schema=False,
response_model=PylonHash,
)
def get_pylon_email_hash(email: str = Query(...)) -> PylonHash:
no_hash = "???-no-hash-???"
secret = settings.PYLON_IDENTITY_VERIFICATION_SECRET
if not secret:
LOG.error("No Pylon identity verification secret", email=email)
return PylonHash(hash=no_hash)
try:
secret_bytes = bytes.fromhex(secret)
signature = hmac.new(secret_bytes, email.encode(), hashlib.sha256).hexdigest()
return PylonHash(hash=signature)
except Exception:
LOG.exception("Failed to generate Pylon email hash", email=email)
return PylonHash(hash=no_hash)

View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel
class PylonHash(BaseModel):
hash: str