Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 97 additions & 35 deletions app/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { getClientConfig } from "../config/client";
import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { PROXY_MODES, ProxyMode } from "../store/access";
import { ProviderType } from "../utils/cloud";
import { TTSConfigList } from "./tts-config";
import { RealtimeConfigList } from "./realtime-chat/realtime-config";
Expand Down Expand Up @@ -737,6 +738,66 @@ export function Settings() {
></input>
</ListItem>
);
const desktopProxyConfigComponent = clientConfig?.isApp && (
<>
<ListItem
title="Desktop Proxy Mode"
subTitle="Choose system, HTTP, or SOCKS5 proxy for desktop requests"
>
<Select
value={accessStore.proxyMode}
onChange={(e) =>
accessStore.update(
(access) => (access.proxyMode = e.target.value as ProxyMode),
)
}
>
{PROXY_MODES.map((mode) => (
<option value={mode} key={mode}>
{mode === "system"
? "System"
: mode === "http"
? "HTTP"
: "SOCKS5"}
</option>
))}
</Select>
</ListItem>

{accessStore.proxyMode !== "system" && (
<>
<ListItem
title="Desktop Proxy Host"
subTitle="Proxy server hostname or IP address"
>
<input
type="text"
value={accessStore.proxyHost}
placeholder="127.0.0.1"
onChange={(e) =>
accessStore.update(
(access) => (access.proxyHost = e.currentTarget.value),
)
}
></input>
</ListItem>

<ListItem title="Desktop Proxy Port" subTitle="Proxy server port">
<input
type="text"
value={accessStore.proxyPort}
placeholder="7890"
onChange={(e) =>
accessStore.update(
(access) => (access.proxyPort = e.currentTarget.value),
)
}
></input>
</ListItem>
</>
)}
</>
);

const openAIConfigComponent = accessStore.provider ===
ServiceProvider.OpenAI && (
Expand Down Expand Up @@ -1459,44 +1520,44 @@ export function Settings() {
</>
);

const ai302ConfigComponent = accessStore.provider === ServiceProvider["302.AI"] && (
const ai302ConfigComponent = accessStore.provider ===
ServiceProvider["302.AI"] && (
<>
<ListItem
title={Locale.Settings.Access.AI302.Endpoint.Title}
subTitle={
Locale.Settings.Access.AI302.Endpoint.SubTitle +
AI302.ExampleEndpoint
title={Locale.Settings.Access.AI302.Endpoint.Title}
subTitle={
Locale.Settings.Access.AI302.Endpoint.SubTitle + AI302.ExampleEndpoint
}
>
<input
aria-label={Locale.Settings.Access.AI302.Endpoint.Title}
type="text"
value={accessStore.ai302Url}
placeholder={AI302.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.ai302Url = e.currentTarget.value),
)
}
>
<input
aria-label={Locale.Settings.Access.AI302.Endpoint.Title}
type="text"
value={accessStore.ai302Url}
placeholder={AI302.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.ai302Url = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.AI302.ApiKey.Title}
subTitle={Locale.Settings.Access.AI302.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.AI302.ApiKey.Title}
value={accessStore.ai302ApiKey}
type="text"
placeholder={Locale.Settings.Access.AI302.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.ai302ApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.AI302.ApiKey.Title}
subTitle={Locale.Settings.Access.AI302.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.AI302.ApiKey.Title}
value={accessStore.ai302ApiKey}
type="text"
placeholder={Locale.Settings.Access.AI302.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.ai302ApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
);

return (
Expand Down Expand Up @@ -1868,6 +1929,7 @@ export function Settings() {
)}
</>
)}
{desktopProxyConfigComponent}

{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
<ListItem
Expand Down
7 changes: 7 additions & 0 deletions app/store/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { DEFAULT_CONFIG } from "./config";
import { getModelProvider } from "../utils/model";

let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
export const PROXY_MODES = ["system", "http", "socks5"] as const;
export type ProxyMode = (typeof PROXY_MODES)[number];

const isApp = getClientConfig()?.buildMode === "export";

Expand Down Expand Up @@ -139,6 +141,11 @@ const DEFAULT_ACCESS_STATE = {
ai302Url: DEFAULT_AI302_URL,
ai302ApiKey: "",

// desktop proxy mode
proxyMode: "system" as ProxyMode,
proxyHost: "127.0.0.1",
proxyPort: "7890",

// server config
needCode: true,
hideUserApiKey: false,
Expand Down
12 changes: 12 additions & 0 deletions app/utils/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
// see src-tauri/src/stream.rs, and src-tauri/src/main.rs
// 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
// 2. listen event: `stream-response` multi times to get body
import { useAccessStore } from "../store/access";

function getDesktopProxyUrl(): string | undefined {
const access = useAccessStore.getState();
if (access.proxyMode === "system") return undefined;
const host = access.proxyHost?.trim().replace(/^(https?|socks5):\/\//i, "");
const port = access.proxyPort?.trim() ?? "";
if (!host) return undefined;
const hp = host.includes(":") ? host : port ? `${host}:${port}` : host;
return `${access.proxyMode}://${hp}`;
}

type ResponseEvent = {
id: number;
Expand Down Expand Up @@ -79,6 +90,7 @@ export function fetch(url: string, options?: RequestInit): Promise<Response> {
method: method.toUpperCase(),
url,
headers,
proxy_url: getDesktopProxyUrl(),
// TODO FormData
body:
typeof body === "string"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ tauri = { version = "1.5.4", features = [ "http-all",
] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
percent-encoding = "2.3.1"
reqwest = "0.11.18"
reqwest = { version = "0.11.18", features = ["socks"] }
futures-util = "0.3.30"
bytes = "1.7.2"

Expand Down
18 changes: 15 additions & 3 deletions src-tauri/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::error::Error;
use std::sync::atomic::{AtomicU32, Ordering};
use std::collections::HashMap;
use futures_util::{StreamExt};
use reqwest::Client;
use reqwest::{Client, Proxy};
use reqwest::header::{HeaderName, HeaderMap};

static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0);
Expand Down Expand Up @@ -38,6 +38,7 @@ pub async fn stream_fetch(
url: String,
headers: HashMap<String, String>,
body: Vec<u8>,
proxy_url: Option<String>,
) -> Result<StreamResponse, String> {

let event_name = "stream-response";
Expand All @@ -54,10 +55,21 @@ pub async fn stream_fetch(
// println!("headers: {:?}", _headers);

let method = method.parse::<reqwest::Method>().map_err(|err| format!("failed to parse method: {}", err))?;
let client = Client::builder()
let mut builder = Client::builder()
.default_headers(_headers)
.redirect(reqwest::redirect::Policy::limited(3))
.connect_timeout(Duration::new(3, 0))
.connect_timeout(Duration::new(3, 0));

if let Some(ref pu) = proxy_url {
let pu = pu.trim().to_string();
if !pu.is_empty() {
let proxy = Proxy::all(&pu)
.map_err(|err| format!("failed to parse proxy url: {}", err))?;
builder = builder.no_proxy().proxy(proxy);
}
}

let client = builder
.build()
.map_err(|err| format!("failed to generate client: {}", err))?;

Expand Down