diff --git a/.electron-builder.config.js b/.electron-builder.config.js index 9e01beb..852f87c 100644 --- a/.electron-builder.config.js +++ b/.electron-builder.config.js @@ -47,9 +47,6 @@ module.exports = async function () { createDesktopShortcut: true, createStartMenuShortcut: true, shortcutName: 'chrome-power', - win: { - icon: "buildResources/icon.ico", - } }, win: { diff --git a/README.md b/README.md index 05050fd..a1324cd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ --- -首款开源指纹浏览器。基于Puppeteer、Electron、React开发。 +首款开源指纹浏览器。基于 Puppeteer、Electron、React 开发。 此软件遵循AGPL协议,因此如果你想对其进行修改并进行商业发布,请保持开源。 @@ -16,9 +16,8 @@ 按照以下步骤开始使用此软件: -- 下载安装包(仅支持Windows) [点击此处下载](https://github.com/zmzimpl/chrome-power-app/releases/download/v1.0.0/chrome-power-Setup-1.0.0.exe) -- 运行安装程序以完成安装 -- 注册账户并登录 +- 下载安装包(仅支持Windows) [点击此处下载](https://github.com/zmzimpl/chrome-power-app/releases) +- 以管理员身份运行安装程序以完成安装 - 强烈建议前往设置页面设置你的缓存目录。 - 创建代理 - 创建窗口 @@ -26,7 +25,6 @@ - 导入窗口 - 从模板导入 - 从AdsPower导入 -- 同步功能正在开发中,目前仅支持平铺所有ChromePower窗口。 ## Todo diff --git a/buildResources/icon.ico b/buildResources/icon.ico index 6b4fd8c..d897f2e 100644 Binary files a/buildResources/icon.ico and b/buildResources/icon.ico differ diff --git a/package-lock.json b/package-lock.json index e2d190f..84a8495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "i18next": "^23.7.8", "i18next-browser-languagedetector": "^7.2.0", "install": "^0.13.0", + "ip": "^2.0.1", "ip2location-nodejs": "^9.6.1", "knex": "^3.0.1", "lodash": "^4.17.21", @@ -7618,6 +7619,11 @@ "node": ">= 0.10" } }, + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -21860,6 +21866,11 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" }, + "ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + }, "ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", diff --git a/package.json b/package.json index 737835a..2e566ab 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "i18next": "^23.7.8", "i18next-browser-languagedetector": "^7.2.0", "install": "^0.13.0", + "ip": "^2.0.1", "ip2location-nodejs": "^9.6.1", "knex": "^3.0.1", "lodash": "^4.17.21", diff --git a/packages/main/src/fingerprint/index.ts b/packages/main/src/fingerprint/index.ts index 966993f..61634ad 100644 --- a/packages/main/src/fingerprint/index.ts +++ b/packages/main/src/fingerprint/index.ts @@ -21,6 +21,7 @@ import api from '../../../shared/api/api'; import {getSettings} from '../utils/get-settings'; import {getPort} from '../server/index'; import {randomFingerprint} from '../services/windowService'; +import { getClientPort } from '../mainWindow'; const logger = createLogger(WINDOW_LOGGER_LABEL); @@ -75,9 +76,9 @@ async function connectBrowser(port: number, ipInfo: IP, windowId: number) { : await browser.newPage(); try { await attachFingerprintToPuppeteer(page, ipInfo); - if (import.meta.env.VITE_START_PAGE_URL) { + if (getClientPort()) { await page.goto( - `${import.meta.env.VITE_START_PAGE_URL}?windowId=${windowId}&serverPort=${getPort()}`, + `http://localhost:${getClientPort()}/#/start?windowId=${windowId}&serverPort=${getPort()}`, ); } } catch (error) { diff --git a/packages/main/src/fingerprint/prepare.ts b/packages/main/src/fingerprint/prepare.ts index e0cd5a0..02ab92b 100644 --- a/packages/main/src/fingerprint/prepare.ts +++ b/packages/main/src/fingerprint/prepare.ts @@ -10,20 +10,38 @@ import {SocksProxyAgent} from 'socks-proxy-agent'; import {ProxyDB} from '../db/proxy'; import {PIN_URL} from '../../../shared/constants'; import {db} from '../db'; -import { getOrigin } from '../server'; +import {getOrigin} from '../server'; +import {bridgeMessageToUI} from '../mainWindow'; +import type {AxiosProxyConfig} from 'axios'; const logger = createLogger(API_LOGGER_LABEL); const getRealIP = async (proxy: DB.Proxy) => { const requestProxy = getRequestProxy(proxy.proxy!, proxy.proxy_type!); + + const makeRequest = async (url: string, proxy: AxiosProxyConfig | undefined) => { + try { + const {data} = await axios.get(url, { + proxy: proxy, + timeout: 5_000, + }); + return url.includes('ip-api.com') ? data.query : data.ip; + } catch (error) { + throw new Error(`Failed to fetch IP from ${url}: ${error}`); + } + }; + try { - const {data} = await axios.get('https://api64.ipify.org?format=json', { - proxy: requestProxy, - timeout: 5_000, - }); - return data.ip; + return await Promise.race([ + makeRequest('http://ip-api.com/json/?fields=61439', requestProxy), + makeRequest('https://api64.ipify.org?format=json', requestProxy), + ]); } catch (error) { - logger.error(`| Prepare | getRealIP | error: ${error}`); + bridgeMessageToUI({ + type: 'error', + text: `获取真实IP失败: ${(error as {message: string}).message}`, + }); + logger.error(`| Prepare | getRealIP | error: ${(error as {message: string}).message}`); } }; diff --git a/packages/main/src/mainWindow.ts b/packages/main/src/mainWindow.ts index 1e2c6a6..47bb017 100644 --- a/packages/main/src/mainWindow.ts +++ b/packages/main/src/mainWindow.ts @@ -1,5 +1,25 @@ import {app, BrowserWindow, ipcMain, shell} from 'electron'; import {join, resolve} from 'node:path'; +import express from 'express'; +import * as portscanner from 'portscanner'; +import type { BridgeMessage } from '../../shared/types/common'; + +const server = express(); +const isDev = import.meta.env.DEV; +let serverStarted = false; +let PORT = 5173; + +// 仅在生产环境下启动Express服务器 +async function findAvailablePortAndStartServer() { + if (!isDev) { + PORT = await portscanner.findAPortNotInUse(5173, 8000); + server.use(express.static(resolve(__dirname, '../../renderer/dist'))); + server.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + serverStarted = true; + }); + } +} async function createWindow() { const browserWindow = new BrowserWindow({ @@ -72,22 +92,34 @@ async function createWindow() { * Load from the Vite dev server for development. */ await browserWindow.loadURL(import.meta.env.VITE_DEV_SERVER_URL); + } else if (serverStarted) { + await browserWindow.loadURL(`http://localhost:${PORT}/index.html`); // 确保端口号与你的服务器端口匹配 } else { - /** - * Load from the local file system for production and test. - * - * Use BrowserWindow.loadFile() instead of BrowserWindow.loadURL() for WhatWG URL API limitations - * when path contains special characters like `#`. - * Let electron handle the path quirks. - * @see https://github.com/nodejs/node/issues/12682 - * @see https://github.com/electron/electron/issues/6869 - */ await browserWindow.loadFile(resolve(__dirname, '../../renderer/dist/index.html')); } return browserWindow; } +export async function initApp() { + await findAvailablePortAndStartServer(); + const mainWindow = await createWindow(); + return mainWindow; +} + +export function getClientPort() { + return PORT; +} + +export function getMainWindow() { + return BrowserWindow.getAllWindows()[0]; +} + +export function bridgeMessageToUI(msg: BridgeMessage) { + const mainWindow = getMainWindow(); + mainWindow?.webContents.send('bridge-msg', msg); +} + /** * Restore an existing BrowserWindow or Create a new BrowserWindow. */ @@ -95,7 +127,7 @@ export async function restoreOrCreateWindow() { let window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); if (window === undefined) { - window = await createWindow(); + window = await initApp(); } if (window.isMinimized()) { diff --git a/packages/preload/src/bridges/common.ts b/packages/preload/src/bridges/common.ts index ce77e02..7d237af 100644 --- a/packages/preload/src/bridges/common.ts +++ b/packages/preload/src/bridges/common.ts @@ -1,5 +1,6 @@ +import type {IpcRendererEvent} from 'electron'; import {ipcRenderer} from 'electron'; -import type {SettingOptions} from '../../../shared/types/common'; +import type {BridgeMessage, SettingOptions} from '../../../shared/types/common'; export const CommonBridge = { async download(path: string) { @@ -30,4 +31,10 @@ export const CommonBridge = { const result = await ipcRenderer.invoke('common-api'); return result; }, + + onMessaged: (callback: (event: IpcRendererEvent, msg: BridgeMessage) => void) => + ipcRenderer.on('bridge-msg', callback), + + offMessaged: (callback: (event: IpcRendererEvent, msg: BridgeMessage) => void) => + ipcRenderer.off('bridge-msg', callback), }; diff --git a/packages/renderer/src/App.tsx b/packages/renderer/src/App.tsx index fad6e0f..162478c 100644 --- a/packages/renderer/src/App.tsx +++ b/packages/renderer/src/App.tsx @@ -5,10 +5,13 @@ import dayjs from 'dayjs'; import './index.css'; import './styles/antd.css'; -import {Layout, Typography} from 'antd'; +import {Layout, Typography, message} from 'antd'; import {useRoutes, useRoutesMap} from './routes'; import Header from './components/header'; import {useEffect, useState} from 'react'; +import {CommonBridge} from '#preload'; +import {MESSAGE_CONFIG} from './constants'; +import type {BridgeMessage} from '../../shared/types/common'; const {Title} = Typography; @@ -20,14 +23,35 @@ const App = () => { const routes = useRoutes(); const routesMap = useRoutesMap(); const [isVisible, setIsVisible] = useState(false); + const [messageApi, contextHolder] = message.useMessage(MESSAGE_CONFIG); + + useEffect(() => { setTimeout(() => setIsVisible(true), 100); // 延迟显示组件 }, []); const location = useLocation(); + useEffect(() => { + const handleMessaged = (_: Electron.IpcRendererEvent, msg: BridgeMessage) => { + messageApi.open({ + type: msg.type, + content: msg.text, + }); + }; + + CommonBridge?.offMessaged(handleMessaged); + + CommonBridge?.onMessaged(handleMessaged); + + return () => { + CommonBridge?.offMessaged(handleMessaged); + }; + }, []); + return ( + {contextHolder} {/* { }, { key: 'Api', - label: 'Sync', + label: 'Api', }, { key: 'Service', diff --git a/packages/renderer/src/pages/settings/index.tsx b/packages/renderer/src/pages/settings/index.tsx index 7461c69..b755e81 100644 --- a/packages/renderer/src/pages/settings/index.tsx +++ b/packages/renderer/src/pages/settings/index.tsx @@ -54,6 +54,7 @@ const Settings = () => { ...changed, }; setFormValue(newFormValue); + handleSave(newFormValue); }; // type FieldType = SettingOptions; @@ -138,7 +139,7 @@ const Settings = () => { )} -
+ {/*
-
+
*/} ); }; diff --git a/packages/shared/types/common.d.ts b/packages/shared/types/common.d.ts index ae519e4..c45325b 100644 --- a/packages/shared/types/common.d.ts +++ b/packages/shared/types/common.d.ts @@ -6,7 +6,14 @@ export interface OperationResult { export interface SettingOptions { profileCachePath: string; - useLocalChrome: boolean, - localChromePath: string, - chromiumBinPath: string, + useLocalChrome: boolean; + localChromePath: string; + chromiumBinPath: string; +} + +export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading'; + +export interface BridgeMessage { + type: NoticeType; + text: string; } diff --git a/types/env.d.ts b/types/env.d.ts index 1da8a4f..7387dc8 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -30,4 +30,5 @@ interface ImportMeta { } declare module 'portscanner'; -declare module 'geoip-lite'; \ No newline at end of file +declare module 'geoip-lite'; +declare module 'ip'; \ No newline at end of file