Skip to content
Merged
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,6 +1,7 @@
import { randomBytes, randomUUID as cryptoRandomUUID } from "node:crypto";
import * as zlib from "node:zlib";
import { CLIENT_LOG3_DOMAIN } from "./config";
import { fetchWithProxy } from "@main/utils/proxy";

interface NeteaseLogRecord {
time: number;
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 fetchWithProxy(
`${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
12 changes: 9 additions & 3 deletions electron/main/apis/netease/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Netease API 请求层
*
* 核心职责:根据加密方式(weapi / linuxapi / eapi / api / xeapi)构造 URL、headers、form body,
* 处理 cookie 合并、响应解密、状态码归一化。使用 Node 原生 fetch。
* 处理 cookie 合并、响应解密、状态码归一化
*/

import { randomBytes } from "node:crypto";
Expand All @@ -20,6 +20,7 @@ import { cookieObjToString, cookieToJson } from "./cookie";
import * as encrypt from "./crypto";
import { getAnonymousToken, getDeviceId } from "./device";
import { ensureXeapiKey, getXeapiSession, updateXeapiSession } from "./xeapi";
import { fetchWithProxy } from "@main/utils/proxy";

/** 调用方传入的可选参数 */
export interface RequestOptions {
Expand Down Expand Up @@ -266,14 +267,19 @@ export const createRequest = async (

let res: Response;
try {
res = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(8000) });
res = await fetchWithProxy(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) };
throw new NeteaseRequestError(answer);
}

// 收集 set-cookie(Node fetch 通过 headers.getSetCookie 暴露原始多值头)
// 收集 set-cookie
const setCookie =
(res.headers as unknown as { getSetCookie?: () => string[] }).getSetCookie?.() ??
(res.headers.get("set-cookie") ? [res.headers.get("set-cookie") as string] : []);
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 @@ -7,6 +7,7 @@

import { API_DOMAIN, UA_MAP } from "./config";
import { xeapiSign, xeapiDecryptPublicKey, type XeapiPublicKey } from "./crypto";
import { fetchWithProxy } from "@main/utils/proxy";

let publicKeyState: XeapiPublicKey | null = null;
let sessionId = "";
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 fetchWithProxy(`${API_DOMAIN}/api/gorilla/anti/crawler/security/key/get`, {
method: "POST",
headers: {
"User-Agent": UA_MAP.api.android,
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
55 changes: 55 additions & 0 deletions electron/main/utils/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { store } from "@main/store";
import { systemLog } from "@main/utils/logger";
import { fetch as undiciFetch, ProxyAgent, Socks5ProxyAgent } from "undici";
import type { Dispatcher } from "undici";

const PROXY_TEST_URL = "https://www.baidu.com";

let proxyAgent: Dispatcher | null = null;
let proxyAgentUrl = "";

const isManualProxyProtocol = (value: string): value is "http" | "https" | "socks5" =>
value === "http" || value === "https" || value === "socks5";

/** 当前手动代理地址;off 或配置无效时返回 null,保持原生直连行为 */
export const getNetworkProxyUrl = (): string | null => {
const config = store.get("system.networkProxy");
if (!isManualProxyProtocol(config.protocol)) return null;
const host = config.host.trim();
const port = Number(config.port);
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return null;
return `${config.protocol}://${host}:${port}`;
};

const getProxyDispatcher = (): Dispatcher | undefined => {
const url = getNetworkProxyUrl();
if (!url) return undefined;
if (!proxyAgent || proxyAgentUrl !== url) {
proxyAgent?.close().catch(() => {});
proxyAgent = url.startsWith("socks5://") ? new Socks5ProxyAgent(url) : new ProxyAgent(url);
proxyAgentUrl = url;
systemLog.info(`[proxy] node fetch proxy=${url}`);
}
return proxyAgent;
};

/** Node fetch 包装:关闭代理时完全等价于原生 fetch,开启代理时才注入 dispatcher */
export const fetchWithProxy = (input: string | URL, init?: RequestInit): Promise<Response> => {
const dispatcher = getProxyDispatcher();
if (!dispatcher) return fetch(input, init);
return undiciFetch(input, { ...(init as RequestInit), dispatcher } as Parameters<
typeof undiciFetch
>[1]) as unknown as Promise<Response>;
};

/** 测试当前代理是否可用 */
export const testNetworkProxy = async (): Promise<boolean> => {
if (!getNetworkProxyUrl()) return false;
try {
const res = await fetchWithProxy(PROXY_TEST_URL, { signal: AbortSignal.timeout(8000) });
return res.ok;
} catch (err) {
systemLog.warn("[proxy] test failed", err);
return false;
}
};
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"license": "AGPL-3.0",
"license-file": "LICENSE",
"engines": {
"node": ">=22",
"node": ">=22.19.0",
"npm": ">=11",
"pnpm": ">=10"
},
Expand Down Expand Up @@ -57,6 +57,7 @@
"electron-updater": "^6.8.3",
"font-list": "^2.0.2",
"hono": "^4.12.25",
"undici": "^8.7.0",
"ws": "^8.20.1"
},
"devDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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: "off",
host: "127.0.0.1",
port: 7890,
},
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 = "off" | "http" | "https" | "socks5";

/** 网络代理配置 */
export interface NetworkProxySettings {
/** 代理协议;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
14 changes: 13 additions & 1 deletion src/components/settings/SettingsItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,26 @@ const descriptionText = computed(() =>
</SButton>
<SNumberInput
v-else-if="item.type === 'number'"
v-model="model"
:model-value="model"
:min="item.min"
:max="item.max"
:step="item.step"
:unit="item.unit"
:placeholder="item.placeholderKey ? t(item.placeholderKey) : ''"
:disabled="isDisabled"
update-on="blur"
class="w-full"
@update:model-value="applyChange($event)"
/>
<SInput
v-else-if="item.type === 'text'"
:model-value="model"
:placeholder="item.placeholderKey ? t(item.placeholderKey) : ''"
:disabled="isDisabled"
update-on="blur"
clearable
class="w-full"
@update:model-value="applyChange($event)"
/>
Comment on lines +137 to +146
<component
:is="item.component"
Expand Down
Loading