506 lines
16 KiB
JavaScript
506 lines
16 KiB
JavaScript
import { k as toUnixPath, y as Debug, l as getErrorMsg, M as MIN_NODE_VERSION, x as createFilterByPattern, Q as ServerStorageName, U as SecretStorageKey, z as buildSuccessResponse, B as buildFailResponse, F as verifyZod } from './shared/gpt-runner-shared.15a86815.mjs';
|
|
import 'minimatch';
|
|
import 'debug';
|
|
import axios from 'axios';
|
|
import { HttpProxyAgent } from 'http-proxy-agent';
|
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
import { spawn } from 'node:child_process';
|
|
import fs, { promises } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import getCacheDir from 'cachedir';
|
|
import { fileURLToPath } from 'node:url';
|
|
import 'jsonc-parser';
|
|
import launch from 'launch-editor';
|
|
import { kvsLocalStorage } from '@kvs/node-localstorage';
|
|
import open from 'open';
|
|
import fp from 'find-free-ports';
|
|
import ip from 'ip';
|
|
import { fetch, Headers, Request, Response } from 'undici';
|
|
import 'web-streams-polyfill/polyfill';
|
|
import 'zod-to-json-schema';
|
|
import 'zod';
|
|
|
|
function initAxios() {
|
|
const httpProxyUrl = process.env.HTTP_PROXY;
|
|
const httpsProxyUrl = process.env.HTTPS_PROXY;
|
|
if (httpProxyUrl) {
|
|
const httpAgent = new HttpProxyAgent(httpProxyUrl);
|
|
axios.defaults.httpAgent = httpAgent;
|
|
}
|
|
if (httpsProxyUrl) {
|
|
const httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
|
|
axios.defaults.httpsAgent = httpsAgent;
|
|
}
|
|
}
|
|
let axiosInstance = null;
|
|
function getAxiosInstance() {
|
|
if (!axiosInstance) {
|
|
initAxios();
|
|
axiosInstance = axios.create();
|
|
}
|
|
return axiosInstance;
|
|
}
|
|
|
|
class PathUtils {
|
|
static getCurrentDirName(importMetaUrl, getDirname) {
|
|
let dirname = "";
|
|
try {
|
|
dirname = getDirname();
|
|
} catch {
|
|
}
|
|
if (!importMetaUrl)
|
|
return toUnixPath(dirname);
|
|
const __filename = fileURLToPath(importMetaUrl);
|
|
const __dirname = path.dirname(__filename);
|
|
return __dirname;
|
|
}
|
|
static join(...paths) {
|
|
return toUnixPath(path.join(...paths));
|
|
}
|
|
static resolve(...paths) {
|
|
return toUnixPath(path.resolve(...paths));
|
|
}
|
|
static relative(from, to) {
|
|
return toUnixPath(path.relative(from, to));
|
|
}
|
|
static dirname(filePath) {
|
|
return toUnixPath(path.dirname(filePath));
|
|
}
|
|
static extname(filePath) {
|
|
if (!PathUtils.isFile(filePath))
|
|
return "";
|
|
return path.extname(filePath);
|
|
}
|
|
static isFile(filePath) {
|
|
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
}
|
|
static isDirectory(filePath) {
|
|
return fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
|
|
}
|
|
static includeExt(filePath, exts) {
|
|
if (!exts || !Array.isArray(exts))
|
|
return false;
|
|
return exts.some((ext) => filePath.endsWith(ext));
|
|
}
|
|
static isExit(filePath) {
|
|
return fs.existsSync(filePath);
|
|
}
|
|
static isAccessible(filePath, mode) {
|
|
if (!PathUtils.isExit(filePath))
|
|
return false;
|
|
const modeMap = {
|
|
F: fs.constants.F_OK,
|
|
R: fs.constants.R_OK,
|
|
W: fs.constants.W_OK,
|
|
X: fs.constants.X_OK
|
|
};
|
|
const finalMode = modeMap[mode || "F"] || fs.constants.F_OK;
|
|
try {
|
|
fs.accessSync(filePath, finalMode);
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
static getDirPath(filePath) {
|
|
return path.dirname(filePath);
|
|
}
|
|
}
|
|
|
|
async function getGlobalCacheDir(name) {
|
|
const cacheDir = getCacheDir(name);
|
|
await createCacheDir(cacheDir);
|
|
return cacheDir;
|
|
}
|
|
async function createCacheDir(cacheDir) {
|
|
if (await PathUtils.isAccessible(cacheDir, "W"))
|
|
return;
|
|
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
}
|
|
|
|
const _BinaryDownloader = class {
|
|
static async getBinaryPath() {
|
|
const cacheDir = await getGlobalCacheDir("nicepkg-tunnel");
|
|
const binaryPath = path.join(cacheDir, _BinaryDownloader.BINARY_FILENAME);
|
|
return binaryPath;
|
|
}
|
|
static async downloadBinary() {
|
|
const debug = new Debug("tunnel");
|
|
const binaryPath = await _BinaryDownloader.getBinaryPath();
|
|
if (!fs.existsSync(binaryPath)) {
|
|
const binaryUrl = `https://cdn-media.huggingface.co/frpc-gradio-${_BinaryDownloader.VERSION}/${_BinaryDownloader.BINARY_NAME}${_BinaryDownloader.EXTENSION}`;
|
|
debug.log(`Downloading ${binaryUrl} to ${binaryPath}...`);
|
|
try {
|
|
const axios = getAxiosInstance();
|
|
const response = await axios.get(binaryUrl, {
|
|
responseType: "arraybuffer"
|
|
});
|
|
await fs.promises.writeFile(binaryPath, response.data, "binary");
|
|
fs.chmodSync(binaryPath, 493);
|
|
debug.log(`Downloaded success ${binaryUrl} to ${binaryPath}`);
|
|
} catch (err) {
|
|
debug.error(`Failed to download ${binaryUrl}: ${err}`);
|
|
if (err?.response?.status === 403) {
|
|
throw new Error(
|
|
`Cannot set up a share link as this platform is incompatible. Please create a GitHub issue with information about your platform: ${JSON.stringify(
|
|
os.userInfo()
|
|
)}`
|
|
);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
let BinaryDownloader = _BinaryDownloader;
|
|
BinaryDownloader.VERSION = "0.2";
|
|
BinaryDownloader.EXTENSION = os.platform() === "win32" ? ".exe" : "";
|
|
BinaryDownloader.MACHINE = os.arch();
|
|
BinaryDownloader.ARCH = _BinaryDownloader.MACHINE === "x64" ? "amd64" : _BinaryDownloader.MACHINE;
|
|
BinaryDownloader.BINARY_NAME = `frpc_${os.type().toLowerCase()}_${_BinaryDownloader.ARCH.toLowerCase()}`;
|
|
BinaryDownloader.BINARY_FILENAME = `${_BinaryDownloader.BINARY_NAME}_v${_BinaryDownloader.VERSION}`;
|
|
|
|
class TunnelProcess {
|
|
constructor(command) {
|
|
this.proc = null;
|
|
this.command = command;
|
|
}
|
|
async start() {
|
|
const binaryPath = await BinaryDownloader.getBinaryPath();
|
|
await BinaryDownloader.downloadBinary();
|
|
return this._startProcess(binaryPath);
|
|
}
|
|
kill() {
|
|
if (this.proc !== null) {
|
|
this.proc.kill("SIGTERM");
|
|
this.proc = null;
|
|
}
|
|
}
|
|
_startProcess(binary) {
|
|
return new Promise((resolve, reject) => {
|
|
this.proc = spawn(binary, this.command, {
|
|
stdio: ["ignore", "pipe", "pipe"]
|
|
});
|
|
process.once("exit", () => this.kill());
|
|
let output = "";
|
|
this.proc.stdout.on("data", (data) => {
|
|
output += data.toString();
|
|
const match = output.match(/start proxy success: (.+)/);
|
|
if (match) {
|
|
resolve(match[1]);
|
|
output = "";
|
|
}
|
|
});
|
|
this.proc.stderr.on("data", (data) => {
|
|
output += data.toString();
|
|
});
|
|
this.proc.on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
class Tunnel {
|
|
constructor(options) {
|
|
this.GRADIO_API_SERVER_URL = "https://api.gradio.app/v2/tunnel-request";
|
|
const {
|
|
remoteHost,
|
|
remotePort,
|
|
localHost,
|
|
localPort,
|
|
shareToken
|
|
} = options;
|
|
this.url = null;
|
|
this.remoteHost = remoteHost || "";
|
|
this.remotePort = remotePort || 0;
|
|
this.localHost = localHost || "localhost";
|
|
this.localPort = localPort;
|
|
this.shareToken = shareToken || "";
|
|
}
|
|
async initProcess() {
|
|
if (!this.remoteHost || !this.remotePort) {
|
|
try {
|
|
const axios = getAxiosInstance();
|
|
const res = await axios.get(this.GRADIO_API_SERVER_URL);
|
|
const data = res.data;
|
|
this.remoteHost = data[0].host;
|
|
this.remotePort = data[0].port;
|
|
} catch (err) {
|
|
throw new Error(`Failed to fetch ${this.GRADIO_API_SERVER_URL}: ${getErrorMsg(err)}`);
|
|
}
|
|
}
|
|
const command = [
|
|
"http",
|
|
"-n",
|
|
this.shareToken,
|
|
"-l",
|
|
`${this.localPort}`,
|
|
"-i",
|
|
this.localHost,
|
|
"--uc",
|
|
"--sd",
|
|
"random",
|
|
"--ue",
|
|
"--server_addr",
|
|
`${this.remoteHost}:${this.remotePort}`,
|
|
"--disable_log_color"
|
|
];
|
|
this.tunnelProcess = new TunnelProcess(command);
|
|
}
|
|
async startTunnel() {
|
|
if (!this.tunnelProcess)
|
|
await this.initProcess();
|
|
this.url = await this.tunnelProcess.start();
|
|
return this.url;
|
|
}
|
|
kill() {
|
|
if (this.tunnelProcess) {
|
|
console.log(
|
|
`Killing tunnel ${this.localHost}:${this.localPort} <> ${this.url}`
|
|
);
|
|
this.tunnelProcess.kill();
|
|
}
|
|
}
|
|
}
|
|
|
|
function compareVersion(a, b) {
|
|
const aParts = a.split(".");
|
|
const bParts = b.split(".");
|
|
const len = Math.max(aParts.length, bParts.length);
|
|
const stringToNum = (str) => parseInt(str.match(/\d+/)?.[0] || "0") || 0;
|
|
for (let i = 0; i < len; i++) {
|
|
const aPart = stringToNum(aParts[i]) || 0;
|
|
const bPart = stringToNum(bParts[i]) || 0;
|
|
if (aPart > bPart)
|
|
return 1;
|
|
if (aPart < bPart)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
function checkNodeVersion() {
|
|
const currentNodeVersion = process.version;
|
|
if (compareVersion(currentNodeVersion, MIN_NODE_VERSION) < 0)
|
|
return `You are using Node ${currentNodeVersion}, but GPT-Runner requires Node ${MIN_NODE_VERSION}.
|
|
Please upgrade your Node version in https://nodejs.org/en/download`;
|
|
}
|
|
function canUseNodeFetchWithoutCliFlag() {
|
|
const currentNodeVersion = process.version;
|
|
return compareVersion(currentNodeVersion, "18.0.0") > 0;
|
|
}
|
|
function getRunServerEnv() {
|
|
if (!canUseNodeFetchWithoutCliFlag()) {
|
|
return {
|
|
NODE_OPTIONS: "--experimental-fetch",
|
|
NODE_NO_WARNINGS: "1"
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
class FileUtils {
|
|
static async readFile(params) {
|
|
const { filePath, valid = true } = params;
|
|
if (typeof filePath !== "string")
|
|
return "";
|
|
if (valid && !PathUtils.isFile(filePath))
|
|
return "";
|
|
if (!filePath)
|
|
return "";
|
|
return promises.readFile(filePath, { encoding: "utf8" });
|
|
}
|
|
static async writeFile(params) {
|
|
const { filePath, content, overwrite = true, valid = true } = params;
|
|
if (valid) {
|
|
if (!PathUtils.isAccessible(filePath, "W"))
|
|
return;
|
|
if (!PathUtils.isFile(filePath))
|
|
return;
|
|
}
|
|
const dir = PathUtils.getDirPath(filePath);
|
|
if (!PathUtils.isExit(dir))
|
|
await promises.mkdir(dir, { recursive: true });
|
|
if (overwrite)
|
|
await promises.writeFile(filePath, content, { encoding: "utf8" });
|
|
else
|
|
await promises.appendFile(filePath, content, { encoding: "utf8" });
|
|
}
|
|
static async deletePath(fullPath) {
|
|
if (!PathUtils.isAccessible(fullPath, "W"))
|
|
await promises.rm(fullPath, { recursive: true });
|
|
}
|
|
static async ensurePath(params) {
|
|
const { filePath } = params;
|
|
if (!PathUtils.isAccessible(filePath, "W"))
|
|
await promises.mkdir(filePath, { recursive: true });
|
|
}
|
|
static async movePath(params) {
|
|
const { oldPath, newPath } = params;
|
|
if (PathUtils.isAccessible(oldPath, "W"))
|
|
await promises.rename(oldPath, newPath);
|
|
}
|
|
static async travelFiles(params) {
|
|
const { isValidPath, callback } = params;
|
|
const filePath = PathUtils.resolve(params.filePath);
|
|
if (!PathUtils.isAccessible(filePath, "R"))
|
|
return;
|
|
const promises$1 = [];
|
|
if (PathUtils.isDirectory(filePath)) {
|
|
const entries = await promises.readdir(filePath);
|
|
for (const entry of entries) {
|
|
const fullPath = PathUtils.join(filePath, entry);
|
|
if (!PathUtils.isAccessible(filePath, "R"))
|
|
continue;
|
|
const isValid = await isValidPath(fullPath);
|
|
if (isValid)
|
|
promises$1.push(FileUtils.travelFiles({ filePath: fullPath, isValidPath, callback }));
|
|
}
|
|
} else {
|
|
const result = callback(filePath);
|
|
if (result instanceof Promise)
|
|
promises$1.push(result);
|
|
}
|
|
await Promise.allSettled(promises$1);
|
|
}
|
|
static async travelFilesByFilterPattern(params) {
|
|
const { filePath, isValidPath, callback, exts = [], includes = null, excludes = null } = params;
|
|
await FileUtils.travelFiles({
|
|
filePath,
|
|
isValidPath: async (filePath2) => {
|
|
if (exts.length > 0 && PathUtils.isFile(filePath2) && !PathUtils.includeExt(filePath2, exts))
|
|
return false;
|
|
if (!createFilterByPattern(includes)(filePath2))
|
|
return false;
|
|
if (createFilterByPattern(excludes)(filePath2))
|
|
return false;
|
|
if (!isValidPath(filePath2))
|
|
return false;
|
|
return true;
|
|
},
|
|
callback
|
|
});
|
|
}
|
|
}
|
|
|
|
async function launchEditor(params) {
|
|
return new Promise((resolve, reject) => {
|
|
const { path, lineNum, columnNum = 0, editorName, onError } = params;
|
|
const finalPath = lineNum && columnNum ? `${path}:${lineNum}:${columnNum}` : path;
|
|
launch(finalPath, editorName, (error) => {
|
|
if (error) {
|
|
onError?.(error);
|
|
reject(error);
|
|
}
|
|
});
|
|
resolve();
|
|
});
|
|
}
|
|
async function launchEditorByPathAndContent(params) {
|
|
const { path, matchContent, editorName, onError } = params;
|
|
const content = await FileUtils.readFile({ filePath: path });
|
|
let lineNum = 0;
|
|
let columnNum = 0;
|
|
if (matchContent) {
|
|
const matchContentStartIndex = content.indexOf(matchContent);
|
|
if (matchContentStartIndex !== -1) {
|
|
const beforeMatchContent = content.slice(0, matchContentStartIndex);
|
|
lineNum = beforeMatchContent.split("\n").length;
|
|
columnNum = matchContentStartIndex - beforeMatchContent.lastIndexOf("\n");
|
|
}
|
|
}
|
|
await launchEditor({ path, lineNum, columnNum, editorName, onError });
|
|
return { lineNum, columnNum };
|
|
}
|
|
|
|
async function getStorage(storageName) {
|
|
const cacheFolder = await getGlobalCacheDir("gpt-runner-server");
|
|
const storage = await kvsLocalStorage({
|
|
name: storageName,
|
|
storeFilePath: cacheFolder,
|
|
version: 1
|
|
});
|
|
return {
|
|
cacheDir: cacheFolder,
|
|
storage
|
|
};
|
|
}
|
|
|
|
function openInBrowser(props) {
|
|
const { url } = props;
|
|
try {
|
|
open(url);
|
|
} catch (error) {
|
|
throw new Error(`Server is started at ${url} but failed to open browser. ${error}`);
|
|
}
|
|
}
|
|
async function getPort(props) {
|
|
const { defaultPort, autoFreePort, excludePorts } = props;
|
|
if (defaultPort) {
|
|
if (!autoFreePort)
|
|
return defaultPort;
|
|
const canUseDefaultPort = await fp.isFreePort(defaultPort);
|
|
if (canUseDefaultPort)
|
|
return defaultPort;
|
|
}
|
|
const freePorts = await fp.findFreePorts(1, {
|
|
startPort: 3001,
|
|
endPort: 9999,
|
|
isFree: async (port) => {
|
|
if (excludePorts?.includes(port))
|
|
return false;
|
|
return fp.isFreePort(port);
|
|
}
|
|
});
|
|
return freePorts[0];
|
|
}
|
|
function getLocalHostname() {
|
|
return ip.address("public", "ipv4");
|
|
}
|
|
|
|
function addNodejsPolyfill() {
|
|
if (!canUseNodeFetchWithoutCliFlag()) {
|
|
console.log("GPT Runner: add polyfill for fetch", process.version);
|
|
globalThis.fetch = fetch;
|
|
globalThis.Headers = Headers;
|
|
globalThis.Request = Request;
|
|
globalThis.Response = Response;
|
|
}
|
|
}
|
|
|
|
async function getDefaultProxyUrl() {
|
|
let proxyUrl = "";
|
|
try {
|
|
const { storage } = await getStorage(ServerStorageName.SecretsConfig);
|
|
const proxySecret = await storage.get(SecretStorageKey.Proxy);
|
|
proxyUrl = proxySecret?.proxyUrl ?? "";
|
|
} catch (error) {
|
|
console.error("getDefaultProxyUrl error", error);
|
|
}
|
|
if (proxyUrl)
|
|
return proxyUrl;
|
|
["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"].forEach((key) => {
|
|
if (proxyUrl)
|
|
return;
|
|
const upperKey = key.toUpperCase();
|
|
const lowerKey = key.toLowerCase();
|
|
const upperKeyValue = process.env[upperKey] && process.env[upperKey] !== "undefined" ? process.env[upperKey] || "" : "";
|
|
const lowerKeyValue = process.env[lowerKey] && process.env[lowerKey] !== "undefined" ? process.env[lowerKey] || "" : "";
|
|
return proxyUrl = upperKeyValue || lowerKeyValue || "";
|
|
});
|
|
return proxyUrl;
|
|
}
|
|
|
|
function sendSuccessResponse(res, options) {
|
|
return res.status(options.status || 200).json(buildSuccessResponse(options));
|
|
}
|
|
function sendFailResponse(res, options) {
|
|
return res.status(options.status || 400).json(buildFailResponse(options));
|
|
}
|
|
function verifyParamsByZod(params, schema) {
|
|
verifyZod(schema, params);
|
|
}
|
|
|
|
export { FileUtils, PathUtils, Tunnel, addNodejsPolyfill, canUseNodeFetchWithoutCliFlag, checkNodeVersion, compareVersion, getDefaultProxyUrl, getGlobalCacheDir, getLocalHostname, getPort, getRunServerEnv, getStorage, launchEditor, launchEditorByPathAndContent, openInBrowser, sendFailResponse, sendSuccessResponse, verifyParamsByZod };
|