Giter VIP home page Giter VIP logo

unlocking-raycast-with-surge's Introduction

Unlocking-Raycast-With-Surge

Warning

This project is for educational purposes only. Please do not use it for commercial purposes.

利用 Surge 的 MiTM 功能拦截请求,并利用 Docker 服务模拟后端操作,从而实现 Raycast 的激活。

Docker 服务是一个简单的 Raycast 的 API 代理。它允许您在不订阅的情况下使用包括 Raycast AI 、翻译、同步在内的 Pro 功能(但是都需要你拥有自己的 API Key)。实现原理如下:

  • Raycast Pro:在服务端修改 /me/ai/models 等关键请求的返回字段。
  • Raycast AI:对于非 AI Completions 之外的某些请求的返回值修改关键字段,而对于 AI Completions 的请求,将 Raycast 的请求转换格式转发到 OpenAI 的 API,响应后再实时转换格式返回。
  • Translator:目前的翻译功能实现也是基于 OpenAI 的,会很慢,你可以自行换成别的商业 API 。
  • Sync:基于本地 JSON 处理,没有用到数据库,避免了额外的配置,但是会有一定的性能损失。

Docker 服务修改自 yufeikang/raycast_api_proxy。由于我在迁移的时候出现了问题,新建此仓库后失去了 fork 的属性。但保留了 .git 贡献历史。

在开始之前,请确保你拥有以下条件:

  • Surge 激活版本
  • 一台云服务器,拥有 Docker、公网 IP
  • 一个域名
  • 一个 Open AI 格式的 API Key 与对应的 Base URL

本文用到的的服务器配置为:Ubuntu 22.04,1Panel + OpenResty + Docker

鉴于会折腾 MiTM 的朋友应该是有一定基础的,本文我主要给出具体的配置文件,还有自己的一些踩坑。

整体的配置思路如下:

Raycast - Surge MiTM 覆写请求 URL - 云服务器 - Nginx 反向代理 - Docker 后端。

使用方法

在服务端启动后端服务

由于 Raycast 的 AI 功能请求与返回都进行了包装,直接请求类似 OpenAI 的接口并不可行,需要使用一个转接器来模拟 Raycast 的后端转接格式,这就需要我们自行搭建并运行后端。

基于 Docker 的方法

此 Docker 镜像修改自 yufeikang/raycast_api_proxy,修复了一些迁移到服务器上会导致的问题。

docker run --name Raycast \
    -e OPENAI_API_KEY=sk-xxxx \
    -e OPENAI_BASE_URL=https://api.openai.com/v1/ \
    -p 12443:80 -e LOG_LEVEL=INFO -d arthals/raycast-backend:latest

这会在你的服务器上的 12443 端口启动一个 Raycast 的 API 代理服务,你需要自行修改 OPENAI_API_KEYOPENAI_BASE_URL

这里存在两处需要注意的地方:

  1. 原仓库给的示例是 OPENAI_API_BASE,但是由于 openai 库升级了,所以现在需要使用 OPENAI_BASE_URL
  2. 原仓库给的示例有一个 --dns 1.1.1.1 的配置,但是由于众所周知的原因,腾讯云的服务器无法访问这个 DNS 服务器,所以你需要移除这个配置。

注意 OPENAI_BASE_URL 的计算方式如下:

如果你的 API 请求的 URL 是 https://api-proxy.com/v1/chat/completions ,那么就是 https://api-proxy.com/v1/

为了使用此代理,你还需要参照 https://arthals.ink/posts/coding/unlocking-raycast-with-surge 或按照下文设置 Surge MiTM 与服务器 Nginx 配置以支持流式传输。

SSL 证书直接通过 1Panel 的 Nginx(Openresty)搞定就行。与 Surge 和服务器后端均无关。

本仓库只支持服务器使用,本地使用需要能自行签发 SSL 证书。若本地使用建议参照原仓库操作。

目前还有如下环境变量可以使用:

  • DEEPLX_API_KEY:DeepLX 的 API Key,用于翻译功能。
  • DEEPLX_BASE_URL:DeepLX 的 API URL,用于翻译功能。
  • APYHUB_API_KEYApyHub 的 API Key,用于联网搜索的 Function Call。

基于 Python + PM2 的方法

你亦可以简单的使用 Python 和 PM2 来启动服务:

# 安装依赖
pip install -r requirements.txt
# 启动服务
pm2 start 'OPENAI_API_KEY="sk-xxxx" OPENAI_BASE_URL="https://api.openai.com/v1/" uvicorn app.main:app --host 0.0.0.0 --port 12443 --reload' --name Raycast

Nginx 服务端配置

按照正常的 1Panel + OpenResty 配置一个反向代理网站,将 https://custome-backend.self.com/ 代理到 http://127.0.0.1:12443/ 即可。

特别地,你需要修改反向代理的配置文件,移除默认的压缩算法以支持流式传输,你可以参见:lobehub/lobe-chat#531

修改后的反向代理配置文件示例如下:

location ^~ / {
    proxy_pass http://127.0.0.1:12443;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_http_version 1.1;
    add_header Cache-Control no-cache;
    proxy_cache off;  # 关闭缓存
    proxy_buffering off;  # 关闭代理缓冲
    chunked_transfer_encoding on;  # 开启分块传输编码
    tcp_nopush on;  # 开启TCP NOPUSH选项,禁止Nagle算法
    tcp_nodelay on;  # 开启TCP NODELAY选项,禁止延迟ACK算法
    keepalive_timeout 300;  # 设定keep-alive超时时间为65秒
}

Surge MiTM 配置与激活脚本

本质是劫持 Raycast 的请求,将请求转发到服务器上。配置如下:

# surge.conf
[MITM]
skip-server-cert-verify = true
h2 = true
hostname = *.raycast.com
ca-passphrase = ...
ca-p12 = MIIKP...

[Script]
raycast-activate-backend.raycast.com = type=http-request,pattern=^https://backend.raycast.com,max-size=0,debug=1,script-path=activator.js

请注意,你需要将 ca-passphraseca-p12 替换为你自己的 Surge CA 密码与证书。

其中用到的 activator.js 脚本修改自 wibus-wee/activation-script,你需要自行修改其中的 https://custome-backend.self.com 为你的服务器地址。

请将 activator.js 放置在 Surge 配置文件夹下。

Activator.js 内容如下(亦可在本仓库找到):

'use strict';

function transformToString(obj) {
    if (typeof obj === 'object') {
        return JSON.stringify(obj);
    }
    return obj;
}
/**
 * 构建 Surge 响应体
 *
 * @param props 响应体属性
 * @description 该函数将会自动将对象转换为 JSON 字符串,因此你可以直接传入对象
 */
function buildResponse(props) {
    if (props.body) {
        props.body = transformToString(props.body);
    }
    // console.log(props.body);
    $done({
        ...props,
    });
}
/**
 * 发送通知
 *
 * @param title 标题
 * @param subtitle 副标题
 * @param body 内容
 * @description 该函数将会自动将对象转换为 JSON 字符串,因此你可以直接传入对象
 */
function sendNotification(title, subtitle, body) {
    title = transformToString(title);
    subtitle = transformToString(subtitle);
    body = transformToString(body);
    $notification.post(title, subtitle, body);
}
const methods = ['get', 'put', 'delete', 'head', 'options', 'patch', 'post'];
/**
 * 发送请求
 * @param props 请求参数
 * @param callback 回调函数
 */
const httpClient = {};
for (let method of methods) {
    // @ts-ignore
    httpClient[method] = (props, callback) => {
        $httpClient[method](props, callback);
    };
}

/**
 * @url https://backend.raycast.com/api/
 */
function raycastActivate() {
    $done({
        url: $request.url.replace('https://backend.raycast.com', 'https://custome-backend.self.com'),
        headers: $request.headers,
        body: $request.body,
    });
}

const activator = {
    raycast: {
        base: 'https://backend.raycast.com/api/',
        activate: [
            {
                base: '*',
                func: raycastActivate,
            },
        ],
    },
};

const url = $request.url;
/**
 * Determine whether the URL matches the base
 */
function isMatchBase(url, base) {
    if (Array.isArray(base)) {
        for (let item of base) {
            if (url.includes(item)) {
                return true;
            }
        }
        return false;
    }
    return url.includes(base);
}
/**
 * Automatic execution of the corresponding function according to the URL
 */
function launch() {
    for (let module in activator) {
        if (isMatchBase(url, activator[module].base)) {
            for (let key in activator[module]) {
                if (key === 'base') continue;
                if (Array.isArray(activator[module][key])) {
                    for (let custom of activator[module][key]) {
                        // 检查 custom.base 是否为通配符 '*',如果是,则匹配任何以 activator[module].base 开头的URL
                        if (custom.base === '*' && url.startsWith(activator[module].base)) {
                            return custom.func();
                        }
                        // 否则,检查精确匹配
                        else if (url === `${activator[module].base}/${custom.base}`) {
                            return custom.func();
                        }
                    }
                    continue;
                }
                if (typeof activator[module][key] === 'object') {
                    // 检查是否为通配符 '*',如果是,则匹配任何以 activator[module].base 开头的URL
                    if (activator[module][key].base === '*' && url.startsWith(activator[module].base)) {
                        return activator[module][key].func();
                    }
                    if (url === `${activator[module].base}/${activator[module][key].base}`) {
                        return activator[module][key].func();
                    }
                } else if (!url.includes(`${activator[module].base}/${key}`)) {
                    continue;
                }
                if (typeof activator[module][key] === 'function') {
                    return activator[module][key]();
                }
            }
        }
    }
    console.log(`[activator] ${url} is not matched`);
    returnDefaultResponse();
    $done();
    return;
}
function returnDefaultResponse() {
    console.log(`[activator] returnDefaultResponse: ${url} - ${$request.method.toLowerCase()}`);
    // @ts-expect-error
    httpClient[$request.method.toLowerCase()](
        {
            url: $request.url,
            headers: $request.headers,
        },
        (err, response, _data) => {
            if (!_data) {
                console.log('returnDefaultResponse: _data is null', err);
                buildResponse({
                    status: 500,
                    body: {},
                });
            }
            buildResponse({
                status: response.status,
                headers: response.headers,
                body: _data,
            });
        }
    );
}

launch();

在本地启动后端服务

如果你想要在本地使用这个服务,那么你需要设置 httpx 的 verify 为 false,然后为了避免后端请求 backend.raycast.com 又请求回自身,你还需要修改 pass_through 函数,额外设置一个私有的 header,然后再在 Surge script 里判断一下,这样做就可以不做自签名证书使用了。

例如:

# app/main.py
# 修改 http_client = httpx.AsyncClient() 为
http_client = httpx.AsyncClient(verify=False)

# app/utils.py
# 修改 pass_through 函数,原先为:
"""
async def pass_through_request(client: httpx.AsyncClient, request: ProxyRequest):
    ...
    headers["accept-encoding"] = "identity"
    try:
        response = await client.request(
            request.method,
            url,
            headers=headers,
            data=request.body,
            params=request.query_params,
            timeout=60.0,
        )
    ...
"""
# 修改后为:
async def pass_through_request(client: httpx.AsyncClient, request: ProxyRequest):
    ...
    headers["accept-encoding"] = "identity"
    headers["arthals"] = "arthals"
    try:
        response = await client.request(
            request.method,
            url,
            headers=headers,
            data=request.body,
            params=request.query_params,
            timeout=60.0,
        )
    ...

对于 activator.js,你需要修改为:

function raycastActivate() {
    // 检查 $request.headers 是否有 arthals 字段
    if ($request.headers['arthals']) {
        $done({});
        return;
    }
    $done({
        url: $request.url.replace('https://backend.raycast.com', 'http://localhost:12443'),
        headers: $request.headers,
        body: $request.body,
    });
}

Credit

wibus-wee/activation-script

yufeikang/raycast_api_proxy

unlocking-raycast-with-surge's People

Contributors

godruoyi avatar jjaychen1e avatar nohzafk avatar qetesh avatar yufeikang avatar zhuozhiyongde avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

unlocking-raycast-with-surge's Issues

Raycast 不会发请求到服务器,但本地可以

我没有surge. 所以我用/etc/hosts
本地:127.0.0.1 backend.raycast.com
我试过了本地docker部署(和服务器一摸一样的配置(cert 也一样))。可以用。
但我在服务器上部署的时候
远程:subdomain.domain.ltd(this is not my real domain)
我用nginx proxy manager. 12443(https)->443(subdomain)(https)不行(192.168.8.2 port 12443 -> subdomain.domain.tld+letsencrypt ssl certificate。服务器docker logs没有任何访问日志
direct local IP 也不行.(/etc/hosts: 192.168.8.2:12443 raycast.backend.com)
我吧self sign certificate放到keychain始终信任了

同步错误、api不兼容one-api

同步错误报错:

Raycast  |   File "/project/app/main.py", line 515, in proxy_sync_get
Raycast  |     after_time = datetime.fromisoformat(after)
Raycast  | ValueError: Invalid isoformat string: '2024-02-03T05:30:04.229Z'

api不兼容one-api

Raycast  | INFO:httpx:HTTP Request: POST https://oneapi.xxx/v1/chat/completions "HTTP/1.1 200 OK"
Raycast  | ERROR:proxy:Unknown error: list index out of range

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.