Feat: may setup proxy (#238)

* add https proxy

* remove proxy in renderer

* proxy work for openai request

* use proxyAgent to enable system proxy

* add proxy setting

* tweak proxy setting
This commit is contained in:
an-lee
2024-02-01 15:33:37 +08:00
committed by GitHub
parent 93dea4ad54
commit 51a810fdfd
17 changed files with 470 additions and 32 deletions

View File

@@ -235,6 +235,8 @@
"resetAllConfirmation": "It will remove all of your personal data, are you sure?",
"resetSettings": "Reset Settings",
"resetSettingsConfirmation": "It will reset all of your settings, are you sure? The library will not be affected.",
"proxySettings": "Proxy Settings",
"proxyConfigUpdated": "Proxy config updated",
"logoutAndRemoveAllPersonalData": "Logout and remove all personal data",
"logoutAndRemoveAllPersonalSettings": "Logout and remove all personal settings",
"hotkeys": "Hotkeys",

View File

@@ -235,6 +235,8 @@
"resetAllConfirmation": "这将删除您的所有个人数据, 您确定要重置吗?",
"resetSettings": "重置设置选项",
"resetSettingsConfirmation": "您确定要重置个人设置选项吗?资料库不会受影响。",
"proxySettings": "代理设置",
"proxyConfigUpdated": "代理配置已更新",
"logoutAndRemoveAllPersonalData": "退出登录并删除所有个人数据",
"logoutAndRemoveAllPersonalSettings": "退出登录并删除所有个人设置选项",
"hotkeys": "快捷键",

View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, globalShortcut, protocol, net } from "electron";
import { app, BrowserWindow, protocol, net } from "electron";
import path from "path";
import settings from "@main/settings";
import "@main/i18n";

View File

@@ -31,6 +31,7 @@ import Ffmpeg from "@main/ffmpeg";
import whisper from "@main/whisper";
import { hashFile } from "@/utils";
import { WEB_API_URL } from "@/constants";
import proxyAgent from "@main/proxy-agent";
const logger = log.scope("db/models/conversation");
@Table({
@@ -137,36 +138,51 @@ export class Conversation extends Model<Conversation> {
// choose llm based on engine
llm() {
const { httpAgent, fetch } = proxyAgent();
if (this.engine === "enjoyai") {
return new ChatOpenAI({
openAIApiKey: settings.getSync("user.accessToken") as string,
modelName: this.model,
configuration: {
baseURL: `${process.env.WEB_API_URL || WEB_API_URL}/api/ai`,
return new ChatOpenAI(
{
openAIApiKey: settings.getSync("user.accessToken") as string,
modelName: this.model,
configuration: {
baseURL: `${process.env.WEB_API_URL || WEB_API_URL}/api/ai`,
},
temperature: this.configuration.temperature,
n: this.configuration.numberOfChoices,
maxTokens: this.configuration.maxTokens,
frequencyPenalty: this.configuration.frequencyPenalty,
presencePenalty: this.configuration.presencePenalty,
},
temperature: this.configuration.temperature,
n: this.configuration.numberOfChoices,
maxTokens: this.configuration.maxTokens,
frequencyPenalty: this.configuration.frequencyPenalty,
presencePenalty: this.configuration.presencePenalty,
});
{
httpAgent,
// @ts-ignore
fetch,
}
);
} else if (this.engine === "openai") {
const key = settings.getSync("openai.key") as string;
if (!key) {
throw new Error(t("openaiKeyRequired"));
}
return new ChatOpenAI({
openAIApiKey: key,
modelName: this.model,
configuration: {
baseURL: this.configuration.baseUrl,
return new ChatOpenAI(
{
openAIApiKey: key,
modelName: this.model,
configuration: {
baseURL: this.configuration.baseUrl,
},
temperature: this.configuration.temperature,
n: this.configuration.numberOfChoices,
maxTokens: this.configuration.maxTokens,
frequencyPenalty: this.configuration.frequencyPenalty,
presencePenalty: this.configuration.presencePenalty,
},
temperature: this.configuration.temperature,
n: this.configuration.numberOfChoices,
maxTokens: this.configuration.maxTokens,
frequencyPenalty: this.configuration.frequencyPenalty,
presencePenalty: this.configuration.presencePenalty,
});
{
httpAgent,
// @ts-ignore
fetch,
}
);
} else if (this.engine === "googleGenerativeAi") {
const key = settings.getSync("googleGenerativeAi.key") as string;
if (!key) {

View File

@@ -18,12 +18,13 @@ import mainWindow from "@main/window";
import fs from "fs-extra";
import path from "path";
import settings from "@main/settings";
import OpenAI from "openai";
import OpenAI, { type ClientOptions } from "openai";
import { t } from "i18next";
import { hashFile } from "@/utils";
import { Audio, Message } from "@main/db/models";
import log from "electron-log/main";
import { WEB_API_URL } from "@/constants";
import proxyAgent from "@main/proxy-agent";
const logger = log.scope("db/models/speech");
@Table({
@@ -171,10 +172,10 @@ export class Speech extends Model<Speech> {
const filename = `${Date.now()}${extname}`;
const filePath = path.join(settings.userDataPath(), "speeches", filename);
let openaiConfig = {};
let openaiConfig: ClientOptions = {};
if (engine === "enjoyai") {
openaiConfig = {
apiKey: settings.getSync("user.accessToken"),
apiKey: settings.getSync("user.accessToken") as string,
baseURL: `${process.env.WEB_API_URL || WEB_API_URL}/api/ai`,
};
} else if (engine === "openai") {
@@ -187,7 +188,14 @@ export class Speech extends Model<Speech> {
baseURL: baseUrl || defaultConfig.baseUrl,
};
}
const openai = new OpenAI(openaiConfig);
const { httpAgent, fetch } = proxyAgent();
const openai = new OpenAI({
...openaiConfig,
httpAgent,
// @ts-ignore
fetch,
});
const file = await openai.audio.speech.create({
input: text,

View File

@@ -0,0 +1,21 @@
import settings from "@main/settings";
import { HttpsProxyAgent } from "https-proxy-agent";
import { ProxyAgent } from "proxy-agent";
import fetch from "node-fetch";
export default function () {
const proxyConfig = settings.getSync("proxy") as ProxyConfigType;
let proxyAgent = new ProxyAgent();
if (proxyConfig.enabled && proxyConfig.url) {
proxyAgent = new ProxyAgent({
httpAgent: new HttpsProxyAgent(proxyConfig.url),
httpsAgent: new HttpsProxyAgent(proxyConfig.url),
});
}
return {
httpAgent: proxyAgent,
fetch,
};
}

View File

@@ -7,6 +7,7 @@ import os from "os";
import commandExists from "command-exists";
import log from "electron-log";
import * as i18n from "i18next";
import mainWin from "@main/window";
const logger = log.scope("settings");

View File

@@ -64,6 +64,49 @@ main.init = () => {
// TedProvider
tedProvider.registerIpcHandlers();
// proxy
ipcMain.handle("system-proxy-get", (_event) => {
let proxy = settings.getSync("proxy");
if (!proxy) {
proxy = {
enabled: false,
url: "",
};
settings.setSync("proxy", proxy);
}
return proxy;
});
ipcMain.handle("system-proxy-set", (_event, config) => {
if (!config) {
throw new Error("Invalid proxy config");
}
if (config) {
if (!config.url) {
config.enabled = false;
}
}
if (config.enabled && config.url) {
const uri = new URL(config.url);
const proxyRules = `http=${uri.host};https=${uri.host}`;
mainWindow.webContents.session.setProxy({
proxyRules,
});
mainWindow.webContents.session.closeAllConnections();
} else {
mainWindow.webContents.session.setProxy({
mode: "system",
});
mainWindow.webContents.session.closeAllConnections();
}
return settings.setSync("proxy", config);
});
// BrowserView
ipcMain.handle(
"view-load",

View File

@@ -40,6 +40,14 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
return ipcRenderer.invoke("system-preferences-media-access", mediaType);
},
},
proxy: {
get: () => {
return ipcRenderer.invoke("system-proxy-get");
},
set: (config: ProxyConfigType) => {
return ipcRenderer.invoke("system-proxy-set", config);
},
},
},
providers: {
audible: {

View File

@@ -15,3 +15,5 @@ export * from "./balance-settings";
export * from "./reset-settings";
export * from "./reset-all-settings";
export * from "./proxy-settings";

View File

@@ -11,6 +11,7 @@ import {
WhisperSettings,
FfmpegSettings,
OpenaiSettings,
ProxySettings,
GoogleGenerativeAiSettings,
ResetSettings,
ResetAllSettings,
@@ -58,6 +59,8 @@ export const Preferences = () => {
<div className="font-semibold mb-4 capitilized">
{t("advancedSettings")}
</div>
<ProxySettings />
<Separator />
<ResetSettings />
<Separator />
<ResetAllSettings />

View File

@@ -0,0 +1,164 @@
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "i18next";
import {
Button,
Form,
FormField,
FormItem,
FormControl,
Switch,
Input,
toast,
} from "@renderer/components/ui";
import { InfoIcon } from "lucide-react";
import { AppSettingsProviderContext } from "@renderer/context";
import { useContext, useState, useEffect } from "react";
export const ProxySettings = () => {
const { proxy, setProxy } = useContext(AppSettingsProviderContext);
const [ipData, setIpData] = useState(null);
const [editing, setEditing] = useState(false);
const proxyConfigSchema = z.object({
enabled: z.boolean(),
url: z.string().url(),
});
const form = useForm({
mode: "onBlur",
resolver: zodResolver(proxyConfigSchema),
values: {
enabled: proxy?.enabled,
url: proxy?.url,
},
});
const onSubmit = async (data: z.infer<typeof proxyConfigSchema>) => {
if (!data.url) {
data.enabled = false;
}
setProxy({
enabled: data.enabled,
url: data.url,
})
.then(() => {
toast.success(t("proxyConfigUpdated"));
})
.finally(() => {
setEditing(false);
});
};
const checkIp = async () => {
fetch("https://ipapi.co/json")
.then((response) => response.json())
.then((data) => {
setIpData(data);
})
.catch((error) => {
toast.error(error.message);
setIpData(null);
});
};
useEffect(() => {
checkIp();
}, [proxy]);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("proxySettings")}</div>
<div className="text-sm text-muted-foreground mb-2 ml-1">
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="http://proxy:port"
disabled={!editing}
value={field.value || ""}
onChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
{form.getValues("enabled") && ipData && (
<div className="text-sm text-muted-foreground mb-2 ml-1">
<div className="flex items-center space-x-2">
<div>
IP: {ipData.ip} ({ipData.city}, {ipData.country_name})
</div>
<div>
<InfoIcon size={16} className="cursor-pointer" />
</div>
</div>
</div>
)}
</div>
<div className="flex items-center space-x-2 justify-end">
{editing ? (
<>
<Button
variant="secondary"
onClick={(e) => {
setEditing(!editing);
e.preventDefault();
}}
size="sm"
>
{t("cancel")}
</Button>
<Button
variant="default"
onClick={() => setEditing(!editing)}
size="sm"
>
{t("save")}
</Button>
</>
) : (
<Button
variant="secondary"
onClick={(e) => {
setEditing(!editing);
e.preventDefault();
}}
size="sm"
>
{t("edit")}
</Button>
)}
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormControl>
<Switch
disabled={!form.getValues("url")}
checked={field.value}
onCheckedChange={(e) => {
field.onChange(e);
onSubmit(form.getValues());
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
</form>
</Form>
);
};

View File

@@ -22,6 +22,8 @@ type AppSettingsProviderState = {
EnjoyApp?: EnjoyAppType;
language?: "en" | "zh-CN";
switchLanguage?: (language: "en" | "zh-CN") => void;
proxy?: ProxyConfigType;
setProxy?: (config: ProxyConfigType) => Promise<void>;
};
const initialState: AppSettingsProviderState = {
@@ -47,6 +49,7 @@ export const AppSettingsProvider = ({
const [ffmpegConfig, setFfmegConfig] = useState<FfmpegConfigType>(null);
const [ffmpeg, setFfmpeg] = useState<FFmpeg>(null);
const [language, setLanguage] = useState<"en" | "zh-CN">();
const [proxy, setProxy] = useState<ProxyConfigType>();
const EnjoyApp = window.__ENJOY_APP__;
const ffmpegRef = useRef(new FFmpeg());
@@ -58,6 +61,7 @@ export const AppSettingsProvider = ({
fetchFfmpegConfig();
fetchLanguage();
loadFfmpegWASM();
fetchProxyConfig();
}, []);
useEffect(() => {
@@ -171,6 +175,17 @@ export const AppSettingsProvider = ({
setLibraryPath(dir);
};
const fetchProxyConfig = async () => {
const config = await EnjoyApp.system.proxy.get();
setProxy(config);
};
const setProxyConfigHandler = async (config: ProxyConfigType) => {
EnjoyApp.system.proxy.set(config).then(() => {
setProxy(config);
});
};
const validate = async () => {
setInitialized(Boolean(user && libraryPath));
};
@@ -192,6 +207,8 @@ export const AppSettingsProvider = ({
ffmpegConfig,
ffmpeg,
setFfmegConfig,
proxy,
setProxy: setProxyConfigHandler,
initialized,
}}
>

View File

@@ -15,6 +15,10 @@ type EnjoyAppType = {
preferences: {
mediaAccess: (mediaType: "microphone") => Promise<boolean>;
};
proxy: {
get: () => Promise<ProxyConfigType>;
set: (config: ProxyConfigType) => Promise<void>;
};
};
providers: {
audible: {

View File

@@ -158,3 +158,8 @@ type TedIdeaType = {
title: string;
description: string;
};
type ProxyConfigType = {
enabled: boolean;
url: string;
};