Skip to content
Open
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
28 changes: 16 additions & 12 deletions electron/main/apis/netease/core/ncbl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { randomBytes, randomUUID as cryptoRandomUUID } from "node:crypto";
import * as zlib from "node:zlib";
import { net } from "electron";
import { CLIENT_LOG3_DOMAIN } from "./config";

interface NeteaseLogRecord {
Expand Down Expand Up @@ -437,19 +438,22 @@ export const doUpload = async (
): Promise<UploadResult> => {
const payload = encryptNCBL(metaJson, body);
const multipart = buildMultipart(payload);
const resp = await fetch(`${CLIENT_LOG3_DOMAIN}/api/clientlog/encrypt/upload?multiupload=true`, {
method: "POST",
headers: {
"Content-Type": `multipart/form-data; boundary=${multipart.boundary}`,
Referer: "https://music.163.com/di",
"User-Agent": `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`,
"Accept-Encoding": "gzip,deflate",
"Accept-Language": "zh-CN,zh;q=0.8",
Cookie: cookieStr,
const resp = await net.fetch(
`${CLIENT_LOG3_DOMAIN}/api/clientlog/encrypt/upload?multiupload=true`,
{
method: "POST",
headers: {
"Content-Type": `multipart/form-data; boundary=${multipart.boundary}`,
Referer: "https://music.163.com/di",
"User-Agent": `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`,
"Accept-Encoding": "gzip,deflate",
"Accept-Language": "zh-CN,zh;q=0.8",
Cookie: cookieStr,
},
body: new Uint8Array(multipart.body),
signal: AbortSignal.timeout(15000),
},
body: new Uint8Array(multipart.body),
signal: AbortSignal.timeout(15000),
});
);

const text = await resp.text();
let respBody: Record<string, any>;
Expand Down
8 changes: 7 additions & 1 deletion electron/main/apis/netease/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { randomBytes } from "node:crypto";
import { net } from "electron";
import {
API_DOMAIN,
DOMAIN,
Expand Down Expand Up @@ -266,7 +267,12 @@ export const createRequest = async (

let res: Response;
try {
res = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(8000) });
res = await net.fetch(url, {
method: "POST",
headers,
body,
signal: AbortSignal.timeout(8000),
});
} catch (err) {
answer.status = 502;
answer.body = { code: 502, msg: err instanceof Error ? err.message : String(err) };
Expand Down
3 changes: 2 additions & 1 deletion electron/main/apis/netease/core/xeapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 拉取 X25519 公钥包(缓存于进程),首次请求后服务端经响应头下发会话密钥,后续请求复用。
*/

import { net } from "electron";
import { API_DOMAIN, UA_MAP } from "./config";
import { xeapiSign, xeapiDecryptPublicKey, type XeapiPublicKey } from "./crypto";

Expand Down Expand Up @@ -40,7 +41,7 @@ const fetchPublicKey = async (
uid: "",
};

const res = await fetch(`${API_DOMAIN}/api/gorilla/anti/crawler/security/key/get`, {
const res = await net.fetch(`${API_DOMAIN}/api/gorilla/anti/crawler/security/key/get`, {
method: "POST",
headers: {
"User-Agent": UA_MAP.api.android,
Expand Down
3 changes: 3 additions & 0 deletions electron/main/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
dispose as disposePlaybackBridge,
} from "@main/plugins/playbackBridge";
import { registerCacheScheme, handleCacheProtocol } from "@main/utils/protocol";
import { applyNetworkProxy } from "@main/utils/proxy";
import { startServer, stopServer } from "@main/server";
import { initUpdater, disposeUpdater } from "@main/services/updater";
import { coreLog, initLogger } from "@main/utils/logger";
Expand Down Expand Up @@ -101,6 +102,8 @@ export const initApp = (): void => {
electronApp.setAppUserModelId("top.imsyy.splayer-next");
// 注册 cache:// 协议处理
handleCacheProtocol();
// 应用持久化网络代理
void applyNetworkProxy();
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
Expand Down
6 changes: 6 additions & 0 deletions electron/main/ipc/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { isWin } from "@main/utils/config";
import { startServer, stopServer } from "@main/server";
import { setOrpheusProtocolRegistered } from "@main/services/orpheus";
import { setTaskbarThumbnailEnabled } from "@main/services/thumbnail";
import { applyNetworkProxy } from "@main/utils/proxy";

/** 配置写入后的副作用 */
const applyConfigChange = (keyPath: string, value: unknown): void => {
Expand Down Expand Up @@ -70,6 +71,11 @@ const applyConfigChange = (keyPath: string, value: unknown): void => {
case "system.registerOrpheusProtocol":
setOrpheusProtocolRegistered(value as boolean);
break;
case "system.networkProxy.protocol":
case "system.networkProxy.host":
case "system.networkProxy.port":
void applyNetworkProxy();
break;
Comment on lines +74 to +78
case "externalApi.enabled":
void (value ? startServer() : stopServer());
break;
Expand Down
4 changes: 4 additions & 0 deletions electron/main/ipc/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getMainWindow, focusMainWindow } from "@main/window";
import { fetchBytes } from "@main/utils/fetchBytes";
import { logsDir } from "@main/utils/paths";
import { consumePendingOrpheusUrl } from "@main/services/orpheus";
import { testNetworkProxy } from "@main/utils/proxy";

/**
* 注册系统相关的 IPC 事件
Expand Down Expand Up @@ -75,6 +76,9 @@ export const registerSystemIpc = (): void => {
app.exit(0);
});

// 测试当前网络代理
ipcMain.handle("system:testNetworkProxy", () => testNetworkProxy());

// 把封面图 URL 拉成字节回渲染层
// 用于 canvas 取色等需要绕过跨域 tainted 的场景;限定 image/* 响应
ipcMain.handle("system:fetchRemoteBytes", async (_event, url: string) => {
Expand Down
52 changes: 52 additions & 0 deletions electron/main/utils/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { net, session } from "electron";
import type { ProxyConfig } from "electron";
import { store } from "@main/store";
import { systemLog } from "@main/utils/logger";

const MAIN_PARTITION = "persist:main";
const PROXY_TEST_URL = "https://www.baidu.com";
Comment thread
imsyy marked this conversation as resolved.

/**
* 将网络代理配置转换为 Electron ProxyConfig。
* system 模式使用 { mode: "system" } 跟随系统代理;off 直连。
*/
const buildProxyConfig = (): ProxyConfig => {
const config = store.get("system.networkProxy");
if (config.protocol === "system") return { mode: "system" };
if (config.protocol === "off") return { mode: "direct" };
const host = config.host.trim();
const port = Number(config.port);
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return { mode: "direct" };
return { proxyRules: `${config.protocol}://${host}:${port}` };
};

/** 应用网络代理配置到 Electron 会话 */
export const applyNetworkProxy = async (): Promise<void> => {
const proxyConfig = buildProxyConfig();
try {
await Promise.all([
session.defaultSession.setProxy(proxyConfig),
session.fromPartition(MAIN_PARTITION).setProxy(proxyConfig),
]);
systemLog.info(
`[proxy] mode=${proxyConfig.mode ?? "fixed_servers"}${proxyConfig.proxyRules ? ` rules=${proxyConfig.proxyRules}` : ""}`,
);
} catch (err) {
systemLog.warn("[proxy] apply failed", err);
}
};

/** 测试当前代理是否可用 */
export const testNetworkProxy = async (): Promise<boolean> => {
const proxyConfig = buildProxyConfig();
if (proxyConfig.mode === "system") return true;
if (proxyConfig.mode === "direct") return false;
try {
await applyNetworkProxy();
const res = await net.fetch(PROXY_TEST_URL, { signal: AbortSignal.timeout(8000) });
return res.ok;
} catch (err) {
systemLog.warn("[proxy] test failed", err);
return false;
}
};
Comment on lines +40 to +52
1 change: 1 addition & 0 deletions electron/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ declare global {
fileName: string,
) => Promise<{ success: boolean; path?: string; error?: string }>;
relaunch: () => Promise<void>;
testNetworkProxy: () => Promise<boolean>;
onProtocolUrl: (callback: (url: string) => void) => () => void;
consumePendingProtocolUrl: () => Promise<string | null>;
};
Expand Down
2 changes: 2 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ const api = {
ipcRenderer.invoke("system:saveFile", data, defaultName),
// 重启应用
relaunch: () => ipcRenderer.invoke("system:relaunch"),
// 测试当前网络代理
testNetworkProxy: () => ipcRenderer.invoke("system:testNetworkProxy"),
// 订阅主进程下发的 orpheus 唤起 URL
onProtocolUrl: (callback: (url: string) => void) =>
subscribe<string>("protocol:orpheus", callback),
Expand Down
5 changes: 5 additions & 0 deletions shared/defaults/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ export const defaultSystemConfig: SystemConfig = {
uiZoom: 100,
onboardingCompleted: false,
neteaseRealIp: false,
networkProxy: {
protocol: "system",
host: "127.0.0.1",
port: 80,
},
neteaseScrobbleEnabled: false,
neteaseScrobbleMode: "ncbl",
registerOrpheusProtocol: false,
Expand Down
15 changes: 15 additions & 0 deletions shared/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,19 @@ export interface ExternalApiSettings {
port: number;
}

/** 网络代理协议 */
export type NetworkProxyProtocol = "system" | "off" | "http" | "https";

/** 网络代理配置 */
export interface NetworkProxySettings {
/** 代理协议;system 跟随系统,off 直连 */
protocol: NetworkProxyProtocol;
/** 代理服务器地址 */
host: string;
/** 代理服务器端口 */
port: number;
}

/** 外部 API 服务运行时状态 */
export interface ExternalApiStatus {
/** 是否正在监听 */
Expand Down Expand Up @@ -397,6 +410,8 @@ export interface SystemConfig {
onboardingCompleted: boolean;
/** NCM请求注入国内 IP(X-Real-IP/X-Forwarded-For) */
neteaseRealIp: boolean;
/** 网络代理配置 */
networkProxy: NetworkProxySettings;
/** 听歌打卡开关 */
neteaseScrobbleEnabled: boolean;
/** 听歌打卡上报方式 */
Expand Down
10 changes: 10 additions & 0 deletions src/components/settings/SettingsItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ const descriptionText = computed(() =>
:disabled="isDisabled"
class="w-full"
/>
<SInput
v-else-if="item.type === 'text'"
:model-value="model"
:placeholder="item.placeholderKey ? t(item.placeholderKey) : ''"
:disabled="isDisabled"
clearable
class="w-full"
@update:model-value="model = $event"
@blur="applyChange(model)"
/>
Comment on lines +135 to +144
<component
:is="item.component"
v-else-if="item.type === 'custom' && item.component"
Expand Down
23 changes: 23 additions & 0 deletions src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,29 @@
"label": "Taskbar Cover Preview",
"description": "Use the album cover as the window preview when hovering the taskbar; disable to show the system's live window"
},
"networkProxyProtocol": {
"label": "Network Proxy",
"description": "Configure a proxy for all network requests",
"system": "Follow system",
"off": "Proxy off",
"http": "HTTP proxy",
"https": "HTTPS proxy"
},
"networkProxyHost": {
"label": "Proxy server",
"description": "Enter the proxy server address, for example 127.0.0.1",
"placeholder": "127.0.0.1"
},
"networkProxyPort": {
"label": "Proxy port",
"description": "Enter the proxy server port, for example 80"
},
"networkProxyTest": {
"label": "Test proxy",
"description": "Test whether the proxy configuration can connect successfully",
"success": "Proxy is available",
"failed": "Proxy test failed. Check the configuration"
},
"neteaseRealIp": {
"label": "Use Real IP",
"description": "May be restricted overseas or in some regions; enable this to try to resolve it"
Expand Down
23 changes: 23 additions & 0 deletions src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,29 @@
"label": "任务栏封面预览",
"description": "悬停任务栏时以专辑封面作为窗口预览,关闭则显示系统默认的实时窗口"
},
"networkProxyProtocol": {
"label": "网络代理",
"description": "为应用所有网络请求配置代理",
"system": "跟随系统",
"off": "关闭代理",
"http": "HTTP 代理",
"https": "HTTPS 代理"
},
"networkProxyHost": {
"label": "代理服务器地址",
"description": "填写代理服务器地址,例如 127.0.0.1",
"placeholder": "127.0.0.1"
},
"networkProxyPort": {
"label": "代理服务器端口",
"description": "填写代理服务器端口,例如 80"
},
"networkProxyTest": {
"label": "测试代理",
"description": "测试代理配置是否可以正常连通",
"success": "该代理可正常使用",
"failed": "代理测试失败,请检查配置"
},
"neteaseRealIp": {
"label": "使用真实IP地址",
"description": "在海外或部分地区可能会受到限制,可开启此处尝试解决"
Expand Down
52 changes: 52 additions & 0 deletions src/settings/categories/services.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { SettingCategory } from "@/types/settings-schema";
import { useSettingsStore } from "@/stores/settings";
import { toast } from "@/composables/useToast";
import i18n from "@/i18n";
import ExternalApiPanel from "@/components/settings/custom/ExternalApiPanel.vue";
import LastfmPanel from "@/components/settings/custom/LastfmPanel.vue";
import IconLucideGlobe from "~icons/lucide/globe";
Expand All @@ -10,6 +13,55 @@ const servicesCategory: SettingCategory = {
{
id: "network",
items: [
{
key: "networkProxyProtocol",
type: "select",
binding: { store: "settings", path: "system.system.networkProxy.protocol" },
options: [
{ value: "system", labelKey: "settings.networkProxyProtocol.system" },
{ value: "off", labelKey: "settings.networkProxyProtocol.off" },
{ value: "http", labelKey: "settings.networkProxyProtocol.http" },
{ value: "https", labelKey: "settings.networkProxyProtocol.https" },
],
defaultValue: "system",
childrenCondition: () => {
const p = useSettingsStore().system.system.networkProxy.protocol;
return p !== "off" && p !== "system";
},
children: [
Comment thread
imsyy marked this conversation as resolved.
{
key: "networkProxyHost",
type: "text",
binding: { store: "settings", path: "system.system.networkProxy.host" },
defaultValue: "127.0.0.1",
placeholderKey: "settings.networkProxyHost.placeholder",
disabled: () => useSettingsStore().system.system.networkProxy.protocol === "off",
},
{
key: "networkProxyPort",
type: "number",
binding: { store: "settings", path: "system.system.networkProxy.port" },
min: 1,
max: 65535,
defaultValue: 80,
disabled: () => useSettingsStore().system.system.networkProxy.protocol === "off",
},
{
key: "networkProxyTest",
type: "button",
action: async () => {
const { t } = i18n.global;
try {
const ok = await window.api.system.testNetworkProxy();
if (ok) toast.success(t("settings.networkProxyTest.success"));
else toast.error(t("settings.networkProxyTest.failed"));
} catch {
toast.error(t("settings.networkProxyTest.failed"));
}
},
},
],
},
{
key: "neteaseRealIp",
type: "switch",
Expand Down
1 change: 1 addition & 0 deletions src/types/settings-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type SettingWidgetType =
| "color"
| "button"
| "custom"
| "text"
| "number";

/** 选择项 */
Expand Down