├── .npmrc ├── doc ├── img │ ├── home.jpg │ ├── profiles.jpg │ ├── proxies.jpg │ └── setting.jpg ├── mac │ ├── auth.png │ ├── boot.png │ ├── net.png │ ├── nocheck.png │ ├── nocheck2.png │ ├── private.png │ └── mac.md ├── README.zh-CN.md ├── README.ru.md └── README.en.md ├── forge.env.d.ts ├── public ├── icon.ico ├── tray.png ├── images │ ├── sea1.jpg │ ├── sea2.jpg │ ├── sea3.jpg │ ├── sea4.jpg │ ├── sea5.jpg │ ├── sea6.jpg │ ├── star1.jpg │ ├── star2.jpg │ ├── star3.jpg │ ├── star4.jpg │ ├── default.jpg │ ├── harbor.jpg │ ├── shaurma1.jpg │ ├── shaurma2.jpg │ ├── shaurma3.jpg │ ├── shaurma4.jpg │ ├── woman1.jpg │ ├── woman2.jpg │ ├── woman3.jpg │ ├── woman4.jpg │ ├── woman5.jpg │ ├── woman6.png │ ├── gradient1.png │ ├── gradient2.png │ ├── gradient3.png │ ├── gradient4.png │ ├── gradient5.png │ ├── gradient6.png │ ├── gradient7.png │ └── gradient8.png └── json │ └── theme.json ├── src-go ├── api │ ├── api.go │ ├── models │ │ ├── Dns.go │ │ ├── Template.go │ │ ├── Webtest.go │ │ ├── Mihomo.go │ │ ├── Profile.go │ │ └── Getter.go │ ├── job │ │ ├── log.go │ │ ├── alive.go │ │ └── refresh.go │ └── handlers │ │ ├── mihomo.go │ │ ├── dns.go │ │ └── ws.go ├── internal │ ├── em │ │ ├── GeoSite.dat │ │ ├── geoip.metadb │ │ ├── GeoLite2-ASN.mmdb │ │ └── dns.yaml │ ├── embed.go │ └── templates.go ├── pkg │ ├── utils │ │ ├── device_details_other.go │ │ ├── domain.go │ │ ├── code.go │ │ ├── port.go │ │ ├── date.go │ │ ├── singleton.go │ │ ├── device_details_darwin.go │ │ ├── mypool.go │ │ ├── device_details_linux.go │ │ ├── snowflake.go │ │ ├── randstr.go │ │ ├── httpclient_test.go │ │ ├── device_details.go │ │ └── device_details_windows.go │ ├── sys │ │ ├── cmd │ │ │ ├── cmd_darwin.go │ │ │ ├── cmd_linux.go │ │ │ └── cmd_windows.go │ │ ├── proxy │ │ │ ├── proxy.go │ │ │ ├── addr.go │ │ │ └── proxy_windows.go │ │ └── admin │ │ │ ├── admin_darwin.go │ │ │ ├── admin_linux.go │ │ │ └── admin_windows.go │ ├── proxy │ │ └── proxy.go │ ├── cron │ │ └── cron.go │ └── constant │ │ └── constant.go ├── .gitignore ├── main.go └── prizrak │ └── core.go ├── src ├── assets │ ├── images │ │ └── appicon.png │ └── fonts │ │ ├── TwemojiCountryFlags.woff2 │ │ └── nunito-v32-cyrillic_latin-regular.woff2 ├── views │ ├── Crawl.vue │ ├── Setting.vue │ ├── Home.vue │ ├── setting │ │ └── Dns.vue │ ├── rule │ │ └── Ignore.vue │ ├── Rule.vue │ └── Log.vue ├── shims-vue.d.ts ├── components │ ├── dnd │ │ ├── VDContainer │ │ │ └── src │ │ │ │ ├── VDContainer.ts │ │ │ │ └── VDContainer.vue │ │ └── index.ts │ ├── menu │ │ ├── MyBottom.vue │ │ ├── MyRule.vue │ │ ├── Off.vue │ │ ├── Language.vue │ │ └── MyNav.vue │ ├── setting │ │ ├── MyTun.vue │ │ ├── MyPort.vue │ │ └── MyBind.vue │ ├── MyLayout.vue │ ├── MyHr.vue │ ├── MySimpleInput.vue │ ├── MyTitleBar.vue │ ├── MyEvent.vue │ ├── MyEditor.vue │ ├── MyDrop.vue │ └── DeepLinkImportOverlay.vue ├── types │ ├── colorthief.d.ts │ ├── webtest.ts │ ├── profile.ts │ ├── persist.ts │ ├── drag.ts │ └── global.d.ts ├── util │ ├── mihomo.ts │ ├── proxy.ts │ ├── menu.ts │ ├── version.ts │ ├── ws.ts │ ├── format.ts │ ├── axiosRequest.ts │ ├── dashboard.ts │ └── pLoad.ts ├── api │ ├── connections │ │ └── index.ts │ ├── dns │ │ └── index.ts │ ├── mihomo │ │ └── index.ts │ ├── prizrak │ │ └── index.ts │ ├── profiles │ │ └── index.ts │ ├── rule │ │ └── index.ts │ ├── home │ │ └── index.ts │ └── index.ts ├── runtime │ └── index.ts ├── store │ ├── proxiesStore.ts │ ├── homeStore.ts │ ├── deepLinkStore.ts │ ├── menuStore.ts │ ├── settingStore.ts │ └── webStore.ts ├── styles │ └── global.css ├── router │ └── index.ts └── auto-imports.d.ts ├── vite.main.config.ts ├── vite.preload.config.ts ├── index.html ├── .eslintrc.json ├── tsconfig.json ├── src-electron ├── store.ts ├── log.ts ├── preload.ts └── launch.ts ├── vite.config.ts ├── .gitignore ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ -------------------------------------------------------------------------------- /doc/img/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/img/home.jpg -------------------------------------------------------------------------------- /doc/mac/auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/mac/auth.png -------------------------------------------------------------------------------- /doc/mac/boot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/mac/boot.png -------------------------------------------------------------------------------- /doc/mac/net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/mac/net.png -------------------------------------------------------------------------------- /forge.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/icon.ico -------------------------------------------------------------------------------- /public/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/tray.png -------------------------------------------------------------------------------- /src-go/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | var Version string 4 | 5 | var ControllerPort int 6 | -------------------------------------------------------------------------------- /doc/img/profiles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/img/profiles.jpg -------------------------------------------------------------------------------- /doc/img/proxies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/img/proxies.jpg -------------------------------------------------------------------------------- /doc/img/setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/img/setting.jpg -------------------------------------------------------------------------------- /doc/mac/nocheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/mac/nocheck.png -------------------------------------------------------------------------------- /doc/mac/nocheck2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/mac/nocheck2.png -------------------------------------------------------------------------------- /doc/mac/private.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/doc/mac/private.png -------------------------------------------------------------------------------- /public/images/sea1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/sea1.jpg -------------------------------------------------------------------------------- /public/images/sea2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/sea2.jpg -------------------------------------------------------------------------------- /public/images/sea3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/sea3.jpg -------------------------------------------------------------------------------- /public/images/sea4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/sea4.jpg -------------------------------------------------------------------------------- /public/images/sea5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/sea5.jpg -------------------------------------------------------------------------------- /public/images/sea6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/sea6.jpg -------------------------------------------------------------------------------- /public/images/star1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/star1.jpg -------------------------------------------------------------------------------- /public/images/star2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/star2.jpg -------------------------------------------------------------------------------- /public/images/star3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/star3.jpg -------------------------------------------------------------------------------- /public/images/star4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/star4.jpg -------------------------------------------------------------------------------- /public/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/default.jpg -------------------------------------------------------------------------------- /public/images/harbor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/harbor.jpg -------------------------------------------------------------------------------- /public/images/shaurma1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/shaurma1.jpg -------------------------------------------------------------------------------- /public/images/shaurma2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/shaurma2.jpg -------------------------------------------------------------------------------- /public/images/shaurma3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/shaurma3.jpg -------------------------------------------------------------------------------- /public/images/shaurma4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/shaurma4.jpg -------------------------------------------------------------------------------- /public/images/woman1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/woman1.jpg -------------------------------------------------------------------------------- /public/images/woman2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/woman2.jpg -------------------------------------------------------------------------------- /public/images/woman3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/woman3.jpg -------------------------------------------------------------------------------- /public/images/woman4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/woman4.jpg -------------------------------------------------------------------------------- /public/images/woman5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/woman5.jpg -------------------------------------------------------------------------------- /public/images/woman6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/woman6.png -------------------------------------------------------------------------------- /public/images/gradient1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient1.png -------------------------------------------------------------------------------- /public/images/gradient2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient2.png -------------------------------------------------------------------------------- /public/images/gradient3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient3.png -------------------------------------------------------------------------------- /public/images/gradient4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient4.png -------------------------------------------------------------------------------- /public/images/gradient5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient5.png -------------------------------------------------------------------------------- /public/images/gradient6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient6.png -------------------------------------------------------------------------------- /public/images/gradient7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient7.png -------------------------------------------------------------------------------- /public/images/gradient8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/public/images/gradient8.png -------------------------------------------------------------------------------- /src-go/internal/em/GeoSite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/src-go/internal/em/GeoSite.dat -------------------------------------------------------------------------------- /src-go/internal/em/geoip.metadb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/src-go/internal/em/geoip.metadb -------------------------------------------------------------------------------- /src/assets/images/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/src/assets/images/appicon.png -------------------------------------------------------------------------------- /src-go/api/models/Dns.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Dns struct { 4 | Content string `json:"content" yaml:"content"` 5 | } 6 | -------------------------------------------------------------------------------- /src-go/internal/em/GeoLite2-ASN.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/src-go/internal/em/GeoLite2-ASN.mmdb -------------------------------------------------------------------------------- /vite.main.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config 4 | export default defineConfig({}); 5 | -------------------------------------------------------------------------------- /vite.preload.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config 4 | export default defineConfig({}); 5 | -------------------------------------------------------------------------------- /src/assets/fonts/TwemojiCountryFlags.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/src/assets/fonts/TwemojiCountryFlags.woff2 -------------------------------------------------------------------------------- /src/views/Crawl.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/fonts/nunito-v32-cyrillic_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/HEAD/src/assets/fonts/nunito-v32-cyrillic_latin-regular.woff2 -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /src-go/pkg/utils/device_details_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !linux && !darwin 2 | 3 | package utils 4 | 5 | func collectDeviceDetails() DeviceDetails { 6 | return DeviceDetails{} 7 | } 8 | -------------------------------------------------------------------------------- /src/components/dnd/VDContainer/src/VDContainer.ts: -------------------------------------------------------------------------------- 1 | export interface istate { 2 | target: number, 3 | data: any[] 4 | } 5 | export enum eType { 6 | SORT = 'sort', 7 | SWITCH = 'switch', 8 | } 9 | -------------------------------------------------------------------------------- /src-go/.gitignore: -------------------------------------------------------------------------------- 1 | static/dist 2 | dist-ssr 3 | *.local 4 | 5 | # Editor directories and files 6 | .vscode/* 7 | !.vscode/extensions.json 8 | .idea 9 | .DS_Store 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | *.sw? 15 | 16 | pandora/test 17 | vendor 18 | -------------------------------------------------------------------------------- /src/types/colorthief.d.ts: -------------------------------------------------------------------------------- 1 | // 颜色计算 2 | declare module "colorthief" { 3 | export default class ColorThief { 4 | getColor(img: HTMLImageElement): [number, number, number]; 5 | getPalette(img: HTMLImageElement, colorCount?: number): [number, number, number][]; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/webtest.ts: -------------------------------------------------------------------------------- 1 | export type WebTest = { 2 | id: string; // 对应 `json:"id"` 3 | order: number; // 对应 `json:"order"` 4 | title: string; // 对应 `json:"title"` 5 | src: string; // 对应 `json:"src"` 6 | testUrl: string; // 对应 `json:"testUrl"` 7 | delay: number; // 对应 `json:"delay"` 8 | }; 9 | -------------------------------------------------------------------------------- /src-go/api/models/Template.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Template struct { 4 | Id string `json:"id" yaml:"id"` 5 | Order int64 `json:"order" yaml:"order"` 6 | Title string `json:"title" yaml:"title"` 7 | Path string `json:"path" yaml:"path"` 8 | Selected bool `json:"selected" yaml:"selected"` 9 | } 10 | -------------------------------------------------------------------------------- /src-go/api/models/Webtest.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type WebTest struct { 4 | Id string `json:"id" yaml:"id"` 5 | Order int64 `json:"order" yaml:"order"` 6 | Title string `json:"title" yaml:"title"` 7 | Src string `json:"src" yaml:"src"` 8 | TestUrl string `json:"testUrl" yaml:"testUrl"` 9 | Delay int `json:"delay" yaml:"delay"` 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prizrak-Box 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src-go/api/models/Mihomo.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Mihomo struct { 4 | Mode string `json:"mode"` 5 | Proxy bool `json:"proxy"` 6 | Tun bool `json:"tun"` 7 | 8 | Port int `json:"port"` 9 | BindAddress string `json:"bindAddress"` 10 | Stack string `json:"stack"` 11 | Dns bool `json:"dns"` 12 | Ipv6 bool `json:"ipv6"` 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/electron", 13 | "plugin:import/typescript" 14 | ], 15 | "parser": "@typescript-eslint/parser" 16 | } 17 | -------------------------------------------------------------------------------- /src-go/pkg/sys/cmd/cmd_darwin.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | func Command(name string, arg ...string) (string, error) { 10 | c := exec.Command(name, arg...) 11 | out, err := c.CombinedOutput() 12 | if err != nil { 13 | return "", fmt.Errorf("%q: %w: %q", strings.Join(append([]string{name}, arg...), " "), err, out) 14 | } 15 | return strings.TrimSpace(string(out)), nil 16 | } 17 | -------------------------------------------------------------------------------- /src-go/pkg/sys/cmd/cmd_linux.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | func Command(name string, arg ...string) (string, error) { 10 | c := exec.Command(name, arg...) 11 | out, err := c.CombinedOutput() 12 | if err != nil { 13 | return "", fmt.Errorf("%q: %w: %q", strings.Join(append([]string{name}, arg...), " "), err, out) 14 | } 15 | return strings.TrimSpace(string(out)), nil 16 | } 17 | -------------------------------------------------------------------------------- /src-go/pkg/sys/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | // EnableProxy 开启系统代理 4 | func EnableProxy(host string, port int) error { 5 | _ = OnHttp(Addr{ 6 | Host: host, 7 | Port: port, 8 | }) 9 | _ = OnHttps(Addr{ 10 | Host: host, 11 | Port: port, 12 | }) 13 | _ = OnSocks(Addr{ 14 | Host: host, 15 | Port: port, 16 | }) 17 | 18 | return nil 19 | } 20 | 21 | // DisableProxy 关闭代理 22 | func DisableProxy() { 23 | _ = OffAll() 24 | } 25 | -------------------------------------------------------------------------------- /src/util/mihomo.ts: -------------------------------------------------------------------------------- 1 | export function pUpdateMihomo(menuStore: any, settingStore: any, api: any): void { 2 | api.updateMihomo({ 3 | mode: menuStore.rule, 4 | proxy: menuStore.proxy, 5 | tun: menuStore.tun, 6 | 7 | port: Number(settingStore.port), 8 | bindAddress: settingStore.bindAddress, 9 | stack: settingStore.stack, 10 | dns: settingStore.dns, 11 | ipv6: settingStore.ipv6, 12 | }) 13 | } -------------------------------------------------------------------------------- /src-go/pkg/sys/cmd/cmd_windows.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | "syscall" 8 | ) 9 | 10 | func Command(name string, arg ...string) (string, error) { 11 | c := exec.Command(name, arg...) 12 | c.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 13 | out, err := c.CombinedOutput() 14 | if err != nil { 15 | return "", fmt.Errorf("%q: %w: %q", strings.Join(append([]string{name}, arg...), " "), err, out) 16 | } 17 | return strings.TrimSpace(string(out)), nil 18 | } 19 | -------------------------------------------------------------------------------- /src/api/connections/index.ts: -------------------------------------------------------------------------------- 1 | // 关闭连接 2 | const closeConnection = (proxy: any) => function (id: any) { 3 | return proxy.$http.delete('/connections/' + id); 4 | } 5 | 6 | // 关闭所有连接 7 | const closeAllConnection = (proxy: any) => function () { 8 | return proxy.$http.delete('/connections'); 9 | } 10 | 11 | 12 | export default function createConnApi(proxy: any) { 13 | return { 14 | closeConnection: closeConnection(proxy), 15 | closeAllConnection: closeAllConnection(proxy), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types/profile.ts: -------------------------------------------------------------------------------- 1 | export class Profile { 2 | id!: string; 3 | type!: number; // 1: 远程订阅, 2: 本地配置, 3: 爬取合并 4 | title?: string; // 可选 5 | order!: string; 6 | selected?: boolean; // 可选 7 | path!: string; 8 | content?: string | ArrayBuffer; // 可选 9 | used?: bigint; // 可选 10 | available?: bigint; // 可选 11 | total?: bigint; // 可选 12 | expire?: string; // 可选 13 | interval?: string; // 可选 14 | home?: string; // 可选 15 | update?: string; // 可选 16 | template?: string; // 可选 17 | } 18 | -------------------------------------------------------------------------------- /src-go/internal/em/dns.yaml: -------------------------------------------------------------------------------- 1 | dns: 2 | enable: true 3 | ipv6: false 4 | prefer-h3: false 5 | listen: 0.0.0.0:1051 6 | enhanced-mode: fake-ip 7 | fake-ip-range: 198.18.0.1/16 8 | nameserver: 9 | - tls://8.8.4.4 10 | - tls://1.1.1.1 11 | - tls://185.222.222.222 12 | nameserver-policy: 13 | 'geosite:category-ru': 14 | - system 15 | - 77.88.8.8 16 | - 195.208.4.1 17 | - 185.222.222.222 18 | proxy-server-nameserver: 19 | - 77.88.8.8 20 | - 195.208.4.1 21 | - 62.76.76.62 22 | - 185.222.222.222 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "vue", 5 | "vue-router" 6 | ], 7 | "baseUrl": "./", 8 | "paths": { 9 | "@/*": [ 10 | "src/*" 11 | ] 12 | }, 13 | "lib": [ 14 | "ESNext", 15 | "dom" 16 | ], 17 | "target": "ESNext", 18 | "module": "commonjs", 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "esModuleInterop": true, 22 | "noImplicitAny": true, 23 | "sourceMap": true, 24 | "outDir": "dist", 25 | "moduleResolution": "node", 26 | "resolveJsonModule": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/types/persist.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | // 内存缓存 4 | export const memoryCache: Record = {}; 5 | 6 | // 自定义存储(模拟同步) 7 | export const customStorage = { 8 | getItem: (key: string): string | null => { 9 | return memoryCache[key] ?? null; 10 | }, 11 | 12 | setItem: (key: string, value: string): void => { 13 | memoryCache[key] = value; // 先存入缓存 14 | if (window["pxStore"]) { 15 | window["pxStore"].set(key, value) 16 | } 17 | } 18 | }; 19 | 20 | // 持久化配置 21 | export const defaultPersist = { 22 | enabled: true, 23 | storage: customStorage 24 | }; 25 | -------------------------------------------------------------------------------- /src-go/pkg/sys/proxy/addr.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type Addr struct { 10 | Host string 11 | Port int 12 | } 13 | 14 | func (a Addr) String() string { 15 | return fmt.Sprintf("%s:%d", a.Host, a.Port) 16 | } 17 | 18 | func ParseAddr(s string) Addr { 19 | tmp := strings.Split(s, ":") 20 | var ( 21 | host = tmp[0] 22 | port int 23 | ) 24 | if len(tmp) > 1 { 25 | port, _ = strconv.Atoi(tmp[1]) 26 | } 27 | return Addr{ 28 | Host: host, 29 | Port: port, 30 | } 31 | } 32 | 33 | func ParseAddrPtr(s string) *Addr { 34 | addr := ParseAddr(s) 35 | return &addr 36 | } 37 | -------------------------------------------------------------------------------- /src/api/dns/index.ts: -------------------------------------------------------------------------------- 1 | // 获取dns 2 | const getDNS = (proxy: any) => async function () { 3 | return await proxy.$http.get('/pDns'); 4 | } 5 | 6 | // 更新dns 7 | const updateDNS = (proxy: any) => async function (configs: any) { 8 | return await proxy.$http.put('/pDns', configs); 9 | } 10 | 11 | // 切换dns 12 | const switchDNS = (proxy: any) => async function (configs: any) { 13 | return await proxy.$http.post('/pDns/switch', configs); 14 | } 15 | 16 | export default function createDnsApi(proxy: any) { 17 | return { 18 | getDNS: getDNS(proxy), 19 | updateDNS: updateDNS(proxy), 20 | switchDNS: switchDNS(proxy), 21 | } 22 | } -------------------------------------------------------------------------------- /src/views/Setting.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /src/util/proxy.ts: -------------------------------------------------------------------------------- 1 | export type ProxySwitchApi = { 2 | setProxy: (group: string, payload: {name: string}) => Promise; 3 | closeAllConnection?: () => Promise | unknown; 4 | }; 5 | 6 | export async function changeProxyAndCloseConnections( 7 | api: ProxySwitchApi, 8 | groupName: string, 9 | proxyName: string, 10 | ) { 11 | await api.setProxy(groupName, {name: proxyName}); 12 | 13 | if (typeof api.closeAllConnection !== 'function') { 14 | return; 15 | } 16 | 17 | try { 18 | await api.closeAllConnection(); 19 | } catch (error) { 20 | console.error('Failed to close connections after switching proxy', error); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/dnd/index.ts: -------------------------------------------------------------------------------- 1 | // 导入组件 2 | import {App, Component} from 'vue' 3 | import VDContainer from './VDContainer/src/VDContainer.vue' 4 | 5 | interface comp { 6 | [keys: string]: Component 7 | } 8 | 9 | // 存储组件列表 10 | const components: comp = { 11 | VDContainer 12 | } 13 | 14 | function install(Vue: App): void { 15 | const keys = Object.keys(components) 16 | keys.forEach(name => { 17 | const component = components[name] 18 | Vue.component(component.name || name, component) 19 | }) 20 | } 21 | 22 | export default { 23 | // 导出的对象必须具有 install,才能被 Vue.use() 方法安装 24 | install, 25 | // 以下是具体的组件列表 26 | ...components 27 | } 28 | -------------------------------------------------------------------------------- /src-go/internal/embed.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import _ "embed" 4 | 5 | //go:embed em/Template_0.yaml 6 | var Template_0 []byte 7 | 8 | //go:embed em/Template_1.yaml 9 | var Template_1 []byte 10 | 11 | //go:embed em/Template_2.yaml 12 | var Template_2 []byte 13 | 14 | //go:embed em/config_download.yaml 15 | var PrizrakDefaultDownloadConfig []byte 16 | 17 | //go:embed em/geoip.metadb 18 | var GeoIp []byte 19 | 20 | //go:embed em/GeoSite.dat 21 | var GeoSite []byte 22 | 23 | //go:embed em/GeoLite2-ASN.mmdb 24 | var ASN []byte 25 | 26 | //go:embed em/webtest.json 27 | var DefaultWebTest []byte 28 | 29 | //go:embed em/dns.yaml 30 | var DefaultDNS string 31 | 32 | //go:embed em/Model.bin 33 | var ModelBin []byte 34 | -------------------------------------------------------------------------------- /src/util/menu.ts: -------------------------------------------------------------------------------- 1 | import {useMenuStore} from "@/store/menuStore"; 2 | 3 | export function changeMenu(value: string, router: any): void { 4 | let path = '' 5 | if (!value.startsWith("/")) { 6 | path = "/" + value 7 | } 8 | const menuStore = useMenuStore(); 9 | // 对rule特殊处理 10 | if (path === "/Rule") { 11 | path += "/" + menuStore.ruleMenu; 12 | } 13 | menuStore.setPath(path); 14 | router.push(path); 15 | } 16 | 17 | 18 | export function detectLanguage() { 19 | const lang = navigator.language.toLowerCase(); 20 | if (lang.startsWith('zh')) return 'zh'; 21 | if (lang.startsWith('en')) return 'en'; 22 | if (lang.startsWith('ru')) return 'ru'; 23 | return 'en'; // 默认 fallback 英文 24 | } -------------------------------------------------------------------------------- /src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | // 只监听 electron 的消息 2 | export const Events = { 3 | // 只向electron发消息 4 | Emit: ({name, data}: { name: string; data: any }) => { 5 | // @ts-ignore 6 | window.pxTray.emit(name, data) 7 | console.log("emit========", name) 8 | }, 9 | // 只收electron的消息 10 | On: (name: string, callback: (...args: any[]) => void) => { 11 | // @ts-ignore 12 | window.pxTray.on(name, callback); 13 | console.log("on========", name) 14 | }, 15 | }; 16 | 17 | // 获取剪贴板内容 18 | export const Clipboard = { 19 | // @ts-ignore 20 | Text: window["pxClipboard"] 21 | } 22 | 23 | // 打开地址 24 | export const Browser = { 25 | // @ts-ignore 26 | OpenURL: (url: string) => window["pxOpen"](url) 27 | } -------------------------------------------------------------------------------- /src/types/drag.ts: -------------------------------------------------------------------------------- 1 | // FlatSortable 组件的 Props 2 | export interface FlatSortableProps { 3 | dir?: boolean // 是否启用排序方向 4 | modal?: boolean // 是否启用模态行为 5 | } 6 | 7 | export type FlatSortableEmits = { 8 | 'update:open': [value: boolean] 9 | } 10 | 11 | // FlatSortableContent 的 Props 12 | export interface FlatSortableContentProps { 13 | direction?: 'row' | 'column'; // 布局方向,可选 14 | gap?: number; // 间距(像素) 15 | } 16 | 17 | // FlatSortableContent 的 Emits,与 FlatSortable 共享 18 | export type FlatSortableContentEmits = FlatSortableEmits; 19 | 20 | // FlatSortableItem 的 Props 21 | export interface FlatSortableItemProps { 22 | 23 | } 24 | 25 | // FlatSortableItem 的 Emits 26 | export type FlatSortableItemEmits = { 27 | 'select': [event: Event] // 选择事件 28 | }; 29 | -------------------------------------------------------------------------------- /src-go/pkg/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/legiz-ru/prizrak-box/pkg/cache" 6 | "github.com/legiz-ru/prizrak-box/pkg/constant" 7 | sys "github.com/legiz-ru/prizrak-box/pkg/sys/proxy" 8 | ) 9 | 10 | // GetProxyUrl 获取代理 11 | func GetProxyUrl() string { 12 | // 从系统获取 13 | addr, err := sys.GetHttp() 14 | if err == nil && addr != nil { 15 | return fmt.Sprintf("http://%s:%d", addr.Host, addr.Port) 16 | } 17 | 18 | // 从数据库中获取 19 | var mi struct { 20 | BindAddress string `json:"bindAddress"` 21 | Port int `json:"port"` 22 | } 23 | _ = cache.Get(constant.Mihomo, &mi) 24 | if mi.BindAddress != "" { 25 | return fmt.Sprintf("http://%s:%d", mi.BindAddress, mi.Port) 26 | } 27 | 28 | // 都获取不到返回空 29 | return "" 30 | } 31 | -------------------------------------------------------------------------------- /src/api/mihomo/index.ts: -------------------------------------------------------------------------------- 1 | // 获取Mihomo 2 | const getMihomo = (proxy: any) => async function () { 3 | return await proxy.$http.get('/mihomo'); 4 | } 5 | 6 | // 更新Mihomo 7 | const updateMihomo = (proxy: any) => async function (configs: any) { 8 | return await proxy.$http.put('/mihomo', configs); 9 | } 10 | 11 | // 等待 Mihomo 切换完成 12 | const waitRunning = (proxy: any) => async function () { 13 | return await proxy.$http.get('/wait'); 14 | } 15 | 16 | // 获取Mihomo 17 | const getAdmin = (proxy: any) => async function () { 18 | return await proxy.$http.get('/mihomo/admin'); 19 | } 20 | 21 | 22 | export default function createMihomoApi(proxy: any) { 23 | return { 24 | getMihomo: getMihomo(proxy), 25 | updateMihomo: updateMihomo(proxy), 26 | waitRunning: waitRunning(proxy), 27 | getAdmin: getAdmin(proxy), 28 | } 29 | } -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import {AxiosRequest} from "@/util/axiosRequest"; 2 | 3 | // 为 '@/api' 模块提供类型声明 4 | declare module '@/api' { 5 | // 定义一个类型接口,替代 `any`,根据项目实际的 API 结构定义 6 | export interface Api { 7 | proxies: () => Promise; 8 | } 9 | } 10 | 11 | // 为 Vue 的全局属性添加类型声明 12 | declare module '@vue/runtime-core' { 13 | export interface ComponentCustomProperties { 14 | $http: AxiosRequest; // 声明全局 $http 的类型 15 | $t: (key: string) => string; // i18n 16 | } 17 | } 18 | 19 | // 绑定函数 20 | declare global { 21 | interface Window { 22 | pxOs: () => string; 23 | pxDeepLink?: { 24 | onImportProfile: (callback: (data: { rawUrl?: string; url?: string; name?: string } | string) => void) => void; 25 | notifyReady?: () => void | Promise; 26 | }; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src-go/pkg/utils/domain.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | // CheckStringAlphabet 6 | // 7 | // @Description: 检查字符串是否为域名 8 | // @param str 9 | // @return bool 10 | func CheckStringAlphabet(str string) bool { 11 | L := len(str) 12 | if L < 1 { 13 | return false 14 | } 15 | charVariable := str[L-1:] 16 | // ipv6 17 | if strings.Contains(str, ":") { 18 | return false 19 | } 20 | // ipv4 21 | if charVariable >= "0" && charVariable <= "9" { 22 | return false 23 | } 24 | 25 | return true 26 | } 27 | 28 | // Reverse 29 | // 30 | // @Description: 反转域名字符串 31 | // @param s 32 | // @return string 33 | func Reverse(s string) string { 34 | if !CheckStringAlphabet(s) { 35 | return s 36 | } 37 | r := []rune(s) 38 | for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 { 39 | r[i], r[j] = r[j], r[i] 40 | } 41 | 42 | return string(r) 43 | } 44 | -------------------------------------------------------------------------------- /src-go/pkg/utils/code.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // IsYAML 判断是否为 yaml 10 | func IsYAML(data string) bool { 11 | var yml map[string]interface{} 12 | return yaml.Unmarshal([]byte(data), &yml) == nil 13 | } 14 | 15 | // IsJSON 判断字符串是否为合法 JSON 格式 16 | func IsJSON(data string) bool { 17 | var js json.RawMessage 18 | err := json.Unmarshal([]byte(data), &js) 19 | return err == nil 20 | } 21 | 22 | // IsBase64 判断字符串是否是 Base64 编码 23 | func IsBase64(data string) bool { 24 | // 检查是否符合 RawStdEncoding 格式 25 | _, errRaw := base64.RawStdEncoding.DecodeString(data) 26 | if errRaw == nil { 27 | return true 28 | } 29 | 30 | // 检查是否符合 StdEncoding 格式 31 | _, errStd := base64.StdEncoding.DecodeString(data) 32 | if errStd == nil { 33 | return true 34 | } 35 | 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /src-go/pkg/utils/port.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | ) 7 | 8 | // IsPortAvailable 检测地址端口是否被占用 9 | func IsPortAvailable(host string, port int) error { 10 | address := host + ":" + strconv.Itoa(port) 11 | listener, err := net.Listen("tcp", address) 12 | if err != nil { 13 | return err // 端口被占用 14 | } 15 | defer func(listener net.Listener) { 16 | err := listener.Close() 17 | if err != nil { 18 | 19 | } 20 | }(listener) 21 | 22 | return nil // 端口可用 23 | } 24 | 25 | // GetRandomPort 获取一个随机可用端口 26 | func GetRandomPort(host string) (int, error) { 27 | listener, err := net.Listen("tcp", host+":0") // 监听端口 0 28 | if err != nil { 29 | return 0, err 30 | } 31 | defer func(listener net.Listener) { 32 | err := listener.Close() 33 | if err != nil { 34 | 35 | } 36 | }(listener) 37 | return listener.Addr().(*net.TCPAddr).Port, nil 38 | } 39 | -------------------------------------------------------------------------------- /src/store/proxiesStore.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {defaultPersist} from "@/types/persist"; 3 | 4 | export const useProxiesStore = defineStore('proxies', { 5 | state: () => ({ 6 | isHide: false, 7 | isSort: false, 8 | isVertical: false, 9 | active: '', 10 | now: "", 11 | }), 12 | actions: { 13 | setHide(isHide: boolean) { 14 | this.isHide = isHide; 15 | }, 16 | setSort(isSort: boolean) { 17 | this.isSort = isSort; 18 | }, 19 | setVertical(isVertical: boolean) { 20 | this.isVertical = isVertical; 21 | }, 22 | setActive(active: string) { 23 | this.active = active; 24 | }, 25 | setNow(now: string) { 26 | this.now = now; 27 | }, 28 | }, 29 | persist: defaultPersist, 30 | }); 31 | -------------------------------------------------------------------------------- /src-go/pkg/utils/date.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 获取系统本地时区 8 | var localZone, _ = time.LoadLocation("Local") 9 | 10 | var layout = "2006-01-02 15:04" 11 | 12 | // GetDateTime 获取当前时间,并格式化为 "2006-01-02 15:04" 13 | func GetDateTime() string { 14 | return time.Now().In(localZone).Format(layout) 15 | } 16 | 17 | // ParseDateTime 使用 time.ParseInLocation 将字符串解析为 time.Time 类型,并设定本地时区 18 | func ParseDateTime(dateTimeStr string) (time.Time, error) { 19 | parsedTime, err := time.ParseInLocation(layout, dateTimeStr, localZone) 20 | if err != nil { 21 | return time.Time{}, err 22 | } 23 | 24 | return parsedTime, nil 25 | } 26 | 27 | // GetHourDiff 计算当前时间与输入时间的时间差(小时数),返回 int 类型 28 | func GetHourDiff(inputTime time.Time) int { 29 | currentTime := time.Now().In(localZone) // 获取本地时区的当前时间 30 | duration := currentTime.Sub(inputTime) // 计算时间差 31 | 32 | return int(duration.Hours()) // 返回 int 类型的小时数 33 | } 34 | -------------------------------------------------------------------------------- /src-electron/store.ts: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store'; 2 | import path from "path"; 3 | import {ipcMain} from "electron"; 4 | import log from './log'; 5 | 6 | let store: Store 7 | 8 | // 初始化数据库 9 | export function initStore(home: string) { 10 | store = new Store({ 11 | cwd: path.join(home, 'px-electron.db') 12 | }); 13 | 14 | ipcMain.handle('store:get', (event, key) => { 15 | return store.get(key); 16 | }); 17 | 18 | ipcMain.handle('store:set', (event, key, value) => { 19 | store.set(key, value); 20 | }); 21 | 22 | log.info("数据库初始化完成") 23 | } 24 | 25 | // 从数据库获取数据 26 | export const storeGet = (key: string) => { 27 | if (store) { 28 | return store.get(key); 29 | } else { 30 | return undefined; 31 | } 32 | } 33 | 34 | // 往数据库存储数据 35 | export const storeSet = (key: string, value: any) => { 36 | if (store) { 37 | store.set(key, value); 38 | } 39 | } -------------------------------------------------------------------------------- /src-go/pkg/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/go-co-op/gocron" 5 | "github.com/metacubex/mihomo/log" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type Cron struct { 11 | scheduler *gocron.Scheduler 12 | } 13 | 14 | var ( 15 | instance *Cron 16 | once sync.Once 17 | ) 18 | 19 | // GetInstance 获取 Cron 单例 20 | func GetInstance() *Cron { 21 | once.Do(func() { 22 | instance = &Cron{ 23 | scheduler: gocron.NewScheduler(time.Local), 24 | } 25 | }) 26 | return instance 27 | } 28 | 29 | // AddTask 添加任务 30 | func AddTask(name string, interval interface{}, task func()) { 31 | cron := GetInstance() 32 | _, err := cron.scheduler.Every(interval).Do(task) 33 | if err != nil { 34 | log.Infoln("添加任务 %s 失败: %v", name, err) 35 | return 36 | } 37 | log.Infoln("已成功添加任务: %s", name) 38 | } 39 | 40 | // Start 启动调度器 41 | func Start() { 42 | GetInstance().scheduler.StartAsync() 43 | } 44 | 45 | // Stop 停止调度器 46 | func Stop() { 47 | GetInstance().scheduler.Stop() 48 | } 49 | -------------------------------------------------------------------------------- /src/store/homeStore.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {defaultPersist} from "@/types/persist"; 3 | 4 | export const useHomeStore = defineStore('home', { 5 | state: () => ({ 6 | startTime: 0, 7 | os: '', 8 | md5: '', 9 | md6: '', 10 | ip: { 11 | query: '', 12 | regionName: '', 13 | country: '', 14 | city: '', 15 | isp: '', 16 | timezone: '', 17 | as: '', 18 | }, 19 | }), 20 | actions: { 21 | setStartTime(startTime: number) { 22 | this.startTime = startTime; 23 | }, 24 | setOS(os: string) { 25 | this.os = os; 26 | }, 27 | setMd5(md5: string) { 28 | this.md5 = md5; 29 | }, 30 | setMd6(md6: string) { 31 | this.md6 = md6; 32 | }, 33 | setIp(ip: any) { 34 | this.ip = ip; 35 | }, 36 | }, 37 | persist: defaultPersist 38 | }); 39 | -------------------------------------------------------------------------------- /src-go/api/job/log.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/metacubex/mihomo/log" 5 | "github.com/legiz-ru/prizrak-box/pkg/cron" 6 | "github.com/legiz-ru/prizrak-box/pkg/utils" 7 | "os" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | var logLock sync.Mutex 13 | 14 | func LogJob(name string) { 15 | cron.AddTask(name, 15*time.Minute, func() { 16 | if logLock.TryLock() { 17 | defer logLock.Unlock() 18 | } else { 19 | return 20 | } 21 | 22 | // 日志路径 23 | filePath := utils.GetUserHomeDir("logs", name) 24 | // 获取文件信息 25 | fileInfo, err := os.Stat(filePath) 26 | if err != nil { 27 | log.Infoln("无法获取文件[%s]信息:%v", name, err) 28 | return 29 | } 30 | 31 | // 判断文件大小是否超过 5MB 32 | if fileInfo.Size() > 5*1024*1024 { 33 | // 清空文件 34 | err := os.Truncate(filePath, 0) 35 | if err != nil { 36 | log.Errorln("清空文件[%s]失败:%v", name, err) 37 | } else { 38 | log.Infoln("文件[%s]已清空", name) 39 | } 40 | } else { 41 | log.Infoln("[%s]文件大小未超过 5MB", name) 42 | } 43 | 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src-go/pkg/utils/singleton.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gofrs/flock" 6 | ) 7 | 8 | var fileLock *flock.Flock 9 | 10 | // NotSingleton attempts to acquire a lock file to ensure only one instance is running. 11 | // Returns true if another instance is already running. 12 | func NotSingleton(name string) bool { 13 | lockFile := GetUserHomeDir("pid", name) 14 | file, err := CreateFile(lockFile) 15 | if err == nil && file != nil { 16 | _ = file.Close() 17 | } 18 | 19 | fileLock = flock.New(lockFile) 20 | 21 | locked, err := fileLock.TryLock() 22 | if err != nil { 23 | fmt.Printf("Failed to acquire lock for %s: %v\n", name, err) 24 | return true 25 | } 26 | 27 | if !locked { 28 | fmt.Printf("Another instance of %s is already running.\n", name) 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | 35 | // UnlockSingleton should be called before exit to release the lock (optional). 36 | func UnlockSingleton() { 37 | if fileLock != nil { 38 | _ = fileLock.Unlock() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src-go/pkg/utils/device_details_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package utils 4 | 5 | import ( 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func collectDeviceDetails() DeviceDetails { 11 | details := DeviceDetails{} 12 | 13 | if uuidOutput, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output(); err == nil { 14 | for _, line := range strings.Split(string(uuidOutput), "") { 15 | if strings.Contains(line, "IOPlatformUUID") { 16 | parts := strings.Split(line, "=") 17 | if len(parts) == 2 { 18 | details.HWID = strings.Trim(strings.TrimSpace(parts[1]), `"`) 19 | break 20 | } 21 | } 22 | } 23 | } 24 | 25 | details.OS = "macOS" 26 | 27 | if versionOutput, err := exec.Command("sw_vers", "-productVersion").Output(); err == nil { 28 | details.OSVersion = strings.TrimSpace(string(versionOutput)) 29 | } 30 | 31 | if modelOutput, err := exec.Command("sysctl", "-n", "hw.model").Output(); err == nil { 32 | details.Model = strings.TrimSpace(string(modelOutput)) 33 | } 34 | 35 | return details 36 | } 37 | -------------------------------------------------------------------------------- /src-go/api/models/Profile.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/legiz-ru/prizrak-box/pkg/utils" 5 | "math/big" 6 | "time" 7 | ) 8 | 9 | type Profile struct { 10 | Id string `json:"id"` 11 | Type int `json:"type"` // 1: 远程订阅 2:本地配置 3:爬取合并 12 | Title string `json:"title,omitempty"` 13 | Order string `json:"order"` 14 | Selected bool `json:"selected,omitempty"` 15 | Path string `json:"path"` 16 | Content string `json:"content,omitempty"` 17 | Used *big.Int `json:"used,omitempty"` 18 | Available *big.Int `json:"available,omitempty"` 19 | Total *big.Int `json:"total,omitempty"` 20 | Expire string `json:"expire,omitempty"` 21 | Interval string `json:"interval,omitempty"` 22 | Home string `json:"home,omitempty"` 23 | Support string `json:"support,omitempty"` 24 | Update string `json:"update,omitempty"` 25 | Template string `json:"template,omitempty"` 26 | } 27 | 28 | func (p *Profile) GetUpdateTime() time.Time { 29 | dateTime, _ := utils.ParseDateTime(p.Update) 30 | return dateTime 31 | } 32 | 33 | func (p *Profile) SetUpdateTime() { 34 | p.Update = utils.GetDateTime() 35 | } 36 | -------------------------------------------------------------------------------- /src-go/pkg/sys/admin/admin_darwin.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // IsAdmin if the program has administrative privileges. 11 | func IsAdmin() bool { 12 | return os.Getuid() == 0 13 | } 14 | 15 | // KillProcessesByName 杀死所有名字为指定名称的进程 16 | func KillProcessesByName(name string) error { 17 | // 确保目标进程名称不为空 18 | if name == "" { 19 | return fmt.Errorf("process name cannot be empty") 20 | } 21 | 22 | // 使用 ps 和 grep 查找进程,并确保精确匹配进程名称 23 | cmd := exec.Command("bash", "-c", fmt.Sprintf("ps -eo pid,comm | grep -w %s | grep -v grep | awk '{print $1}'", name)) 24 | output, err := cmd.Output() 25 | if err != nil { 26 | return fmt.Errorf("failed to find processes with name %s: %w", name, err) 27 | } 28 | 29 | // 解析输出,获取所有匹配的进程 ID 30 | pids := strings.Fields(string(output)) 31 | if len(pids) == 0 { 32 | return fmt.Errorf("no processes found with name %s", name) 33 | } 34 | 35 | // 遍历所有进程 ID,逐个杀死 36 | for _, pid := range pids { 37 | killCmd := exec.Command("kill", "-9", pid) 38 | err := killCmd.Run() 39 | if err != nil { 40 | return fmt.Errorf("failed to kill process with PID %s: %w", pid, err) 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /src-go/pkg/sys/admin/admin_linux.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // IsAdmin if the program has administrative privileges. 11 | func IsAdmin() bool { 12 | return os.Getuid() == 0 13 | } 14 | 15 | // KillProcessesByName 杀死所有名字为指定名称的进程 16 | func KillProcessesByName(name string) error { 17 | // 确保目标进程名称不为空 18 | if name == "" { 19 | return fmt.Errorf("process name cannot be empty") 20 | } 21 | 22 | // 使用 ps 和 grep 查找进程,并确保精确匹配进程名称 23 | cmd := exec.Command("bash", "-c", fmt.Sprintf("ps -eo pid,comm | grep -w %s | grep -v grep | awk '{print $1}'", name)) 24 | output, err := cmd.Output() 25 | if err != nil { 26 | return fmt.Errorf("failed to find processes with name %s: %w", name, err) 27 | } 28 | 29 | // 解析输出,获取所有匹配的进程 ID 30 | pids := strings.Fields(string(output)) 31 | if len(pids) == 0 { 32 | return fmt.Errorf("no processes found with name %s", name) 33 | } 34 | 35 | // 遍历所有进程 ID,逐个杀死 36 | for _, pid := range pids { 37 | killCmd := exec.Command("kill", "-9", pid) 38 | err := killCmd.Run() 39 | if err != nil { 40 | return fmt.Errorf("failed to kill process with PID %s: %w", pid, err) 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 24 | 25 | 55 | -------------------------------------------------------------------------------- /src-electron/log.ts: -------------------------------------------------------------------------------- 1 | import {app} from 'electron'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import log from 'electron-log/main'; 5 | 6 | // 获取用户根目录路径 7 | const userHomeDir = app.getPath('home'); 8 | 9 | // 定义日志目录和文件路径 10 | const logDir = path.join(userHomeDir, 'Prizrak-Box-V3', 'logs'); 11 | const logFilePath = path.join(logDir, 'px-client.log'); 12 | 13 | // 确保目录存在 14 | if (!fs.existsSync(logDir)) { 15 | fs.mkdirSync(logDir, {recursive: true}); 16 | } 17 | 18 | // 自定义日志文件路径 19 | log.transports.file.resolvePathFn = () => logFilePath; 20 | 21 | // 设置日志等级和最大文件大小等配置 22 | log.transports.file.level = 'info'; // 可以设置日志等级,比如 'info', 'warn', 'error' 23 | log.transports.file.maxSize = 5 * 1024 * 1024; // 设置最大文件大小为 5MB 24 | 25 | // 日志记录功能的封装 26 | export default { 27 | info: (...args: any[]) => { 28 | log.info(...args); 29 | }, 30 | warn: (...args: any[]) => { 31 | log.warn(...args); 32 | }, 33 | error: (...args: any[]) => { 34 | log.error(...args); 35 | }, 36 | debug: (...args: any[]) => { 37 | log.debug(...args); 38 | }, 39 | getLogFilePath: () => { 40 | return logFilePath; 41 | }, 42 | getHomeDir: () => { 43 | return path.join(userHomeDir, 'Prizrak-Box-V3'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /doc/mac/mac.md: -------------------------------------------------------------------------------- 1 |

Mac Часто задаваемые вопросы

2 | 3 | # 1. При первом открытии появляется сообщение «Невозможно проверить разработчика». Решение: 4 |
5 | Сначала нажмите «Отмена»
6 | Затем перейдите в меню  (яблоко) в правом верхнем углу → Системные настройки → Безопасность и конфиденциальность → выбрать «Всё равно открыть» 7 |
8 | 9 | # 2. Сообщение «Невозможно проверить разработчика. Точно открыть?». Нажмите «Открыть» 10 |
11 | 12 | # 3. Сообщение «Px требует разрешения для использования режима TUN». Выберите по необходимости 13 |
14 | **Важно: если нажать «Отмена», то в приложении нельзя будет включить TUN**
15 | 16 | # 4. Сообщение «px запрашивает доступ к сетевому подключению». Нажмите «Разрешить» 17 |
18 | 19 | # 5. При настройке автозапуска появляется сообщение «Требуется контроль». Нажмите «OK» 20 |
21 | 22 | # 6. Сообщение «Приложение повреждено». Решение: 23 | Откройте терминал и введите следующую команду. После неё укажите путь к приложению. Не знаете путь? Просто перетащите приложение в окно терминала, и путь отобразится. 24 | 25 | ```shell 26 | xattr -rd com.apple.quarantine /Applications/Prizrak-Box.app 27 | -------------------------------------------------------------------------------- /src/views/setting/Dns.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 48 | 49 | 60 | -------------------------------------------------------------------------------- /src/components/menu/MyBottom.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 47 | 48 | -------------------------------------------------------------------------------- /src-go/api/models/Getter.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/legiz-ru/prizrak-box/pkg/utils" 5 | "time" 6 | ) 7 | 8 | type Getter struct { 9 | Id string `json:"id" yaml:"id"` 10 | Order int64 `json:"order" yaml:"order"` 11 | Content string `json:"content" yaml:"content"` // 可以为任意内容 url base64 json yaml 等 12 | TestUrl string `json:"testUrl" yaml:"testUrl"` 13 | Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` 14 | Cache int `json:"cache,omitempty" yaml:"cache,omitempty"` 15 | Crawl int `json:"crawl,omitempty" yaml:"crawl,omitempty"` 16 | Available int `json:"available,omitempty" yaml:"available,omitempty"` 17 | Interval string `json:"interval,omitempty" yaml:"interval,omitempty"` 18 | Update string `json:"update,omitempty" yaml:"update,omitempty"` 19 | } 20 | 21 | type Yml struct { 22 | Proxies []map[string]any `json:"proxies,omitempty" yaml:"proxies,omitempty"` 23 | } 24 | 25 | type Void struct{} 26 | 27 | type RealIp struct { 28 | Key string `json:"key" yaml:"key"` 29 | CountryCode string `json:"country_code" yaml:"country_code"` 30 | } 31 | 32 | func (g *Getter) GetUpdateTime() time.Time { 33 | dateTime, _ := utils.ParseDateTime(g.Update) 34 | return dateTime 35 | } 36 | 37 | func (g *Getter) SetUpdateTime() { 38 | g.Update = utils.GetDateTime() 39 | } 40 | -------------------------------------------------------------------------------- /src/store/deepLinkStore.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | 3 | interface DeepLinkImportState { 4 | isImporting: boolean; 5 | message: string; 6 | cancelLabel: string; 7 | cancelHandler: (() => void) | null; 8 | } 9 | 10 | interface StartImportPayload { 11 | message: string; 12 | cancelLabel?: string; 13 | onCancel?: () => void; 14 | } 15 | 16 | export const useDeepLinkImportStore = defineStore('deepLinkImport', { 17 | state: (): DeepLinkImportState => ({ 18 | isImporting: false, 19 | message: '', 20 | cancelLabel: '', 21 | cancelHandler: null, 22 | }), 23 | actions: { 24 | startImport(payload: StartImportPayload) { 25 | this.isImporting = true; 26 | this.message = payload.message; 27 | this.cancelLabel = payload.cancelLabel ?? ''; 28 | this.cancelHandler = payload.onCancel ?? null; 29 | }, 30 | finishImport() { 31 | this.isImporting = false; 32 | this.message = ''; 33 | this.cancelLabel = ''; 34 | this.cancelHandler = null; 35 | }, 36 | cancelImport() { 37 | if (!this.isImporting) { 38 | return; 39 | } 40 | const handler = this.cancelHandler; 41 | this.finishImport(); 42 | if (handler) { 43 | handler(); 44 | } 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src-go/pkg/constant/constant.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import _ "embed" 4 | 5 | const ( 6 | DefaultWorkDir = "Prizrak-Box-V3" 7 | DefaultCrawlDir = "crawl" 8 | DefaultTemplateDir = "template" 9 | DefaultServerDB = "px-server.db" 10 | DefaultClientDB = "px-client.db" 11 | DefaultDownload = "Download_0.yaml" 12 | PrefixProfile = "Profile_" 13 | ProfileOrder = "ProfileOrder" 14 | PrefixWebTest = "WebTest_" 15 | WebTestOrder = "WebTestOrder" 16 | PrefixGetter = "Getter_" 17 | PrefixTemplate = "Template_" 18 | TemplateSwitch = "TemplateSwitch" 19 | RealIpHeader = "RealIp_" 20 | SecretKey = "SecretKey_pb" 21 | RecoverTmp = "RecoverTmp" 22 | QuitSignal = "QuitSignal" 23 | Dns = "DNS" 24 | Mihomo = "Mihomo" 25 | TemplateBuiltinVersion = "TemplateBuiltinVersion" 26 | ) 27 | 28 | const ( 29 | CollectLocal = "local" 30 | CollectBatch = "batch" 31 | CollectClash = "clash" 32 | CollectV2ray = "v2ray" 33 | CollectSharelink = "share" 34 | CollectFuzzy = "fuzzy" 35 | CollectAuto = "auto" 36 | CollectSingBox = "sing" 37 | ) 38 | 39 | const PrizrakVersionUrl = "https://raw.githubusercontent.com/legiz-ru/Prizrak-Box/main/backend/constant/version.txt" 40 | const PrizrakDownloadUrl = "https://github.com/legiz-ru/Prizrak-Box/releases/download/%s/%s-%s.zip" 41 | -------------------------------------------------------------------------------- /src/store/menuStore.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {defaultPersist} from "@/types/persist"; 3 | 4 | export const useMenuStore = defineStore('menu', { 5 | state: () => ({ 6 | menu: 'Home', 7 | path: '/Home', 8 | rule: 'rule', 9 | ruleNum: 0, 10 | proxy: false, 11 | tun: false, 12 | language: '', 13 | ruleMenu: 'Now', 14 | background: 'url("/images/default.jpg")', 15 | useWhite: true 16 | }), 17 | actions: { 18 | setMenu(menu: string) { 19 | this.menu = menu; 20 | }, 21 | setPath(path: string) { 22 | this.path = path; 23 | }, 24 | setRule(rule: string) { 25 | this.rule = rule; 26 | }, 27 | setProxy(proxy: boolean) { 28 | this.proxy = proxy; 29 | }, 30 | setTun(tun: boolean) { 31 | this.tun = tun; 32 | }, 33 | setLanguage(language: string) { 34 | this.language = language; 35 | }, 36 | setRuleMenu(ruleMenu: string) { 37 | this.ruleMenu = ruleMenu; 38 | }, 39 | setRuleNum(ruleNum: number) { 40 | this.ruleNum = ruleNum; 41 | }, 42 | setBackground(background: string) { 43 | this.background = background; 44 | }, 45 | setUseWhite(useWhite: boolean) { 46 | this.useWhite = useWhite; 47 | } 48 | }, 49 | persist: defaultPersist, 50 | }); 51 | -------------------------------------------------------------------------------- /src-go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/metacubex/mihomo/hub/executor" 6 | "github.com/metacubex/mihomo/log" 7 | "github.com/legiz-ru/prizrak-box/prizrak" 8 | sys "github.com/legiz-ru/prizrak-box/pkg/sys/proxy" 9 | "github.com/legiz-ru/prizrak-box/pkg/utils" 10 | "go.uber.org/automaxprocs/maxprocs" 11 | "net/url" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | ) 16 | 17 | func main() { 18 | 19 | // 优化线程资源配置 20 | _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) 21 | 22 | // 回调地址 23 | addr := flag.String("addr", "", "callback address") 24 | home := flag.String("home", "", "home directory") 25 | 26 | // 解析命令行参数 27 | flag.Parse() 28 | 29 | if addr == nil || *addr == "" { 30 | panic("callback address is required") 31 | } 32 | 33 | if home == nil || *home == "" { 34 | panic("home directory is required") 35 | } 36 | 37 | homeDir, err := url.QueryUnescape(*home) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // 设置工作目录 43 | utils.InitHomeDir(homeDir) 44 | 45 | // 保持单例 46 | if utils.NotSingleton("px-server.pid") { 47 | os.Exit(1) 48 | } 49 | 50 | // 初始化工作目录 51 | prizrak.Init() 52 | 53 | // 开启后端api 54 | prizrak.StartCore(*addr) 55 | 56 | termSign := make(chan os.Signal, 1) 57 | signal.Notify(termSign, syscall.SIGINT, syscall.SIGTERM) 58 | select { 59 | case <-termSign: 60 | log.Warnln("received termination signal") 61 | prizrak.Release() 62 | utils.UnlockSingleton() 63 | executor.Shutdown() 64 | sys.DisableProxy() 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/api/prizrak/index.ts: -------------------------------------------------------------------------------- 1 | // 开启代理 2 | const enableProxy = (proxy: any) => async function (configs: any) { 3 | return await proxy.$http.put('/prizrak/enableProxy', configs); 4 | } 5 | 6 | // 关闭代理 7 | const disableProxy = (proxy: any) => async function () { 8 | return await proxy.$http.get('/prizrak/disableProxy'); 9 | } 10 | 11 | // 检测地址端口是否可用 12 | const checkAddressPort = (proxy: any) => async function (configs: any) { 13 | return await proxy.$http.put('/prizrak/checkAddressPort', configs); 14 | } 15 | 16 | // 获取配置文件目录 17 | const configDir = (proxy: any) => async function () { 18 | return await proxy.$http.get('/prizrak/configDir'); 19 | } 20 | 21 | // 退出Px 22 | const exit = (proxy: any) => async function () { 23 | return await proxy.$http.get('/prizrak/exit'); 24 | } 25 | 26 | // 更新HTTP客户端配置 27 | type DeviceHeaderDetails = { 28 | hwid: string; 29 | os: string; 30 | osVersion: string; 31 | model: string; 32 | }; 33 | 34 | const updateHTTPClientConfig = (proxy: any) => async function (config: any): Promise { 35 | return await proxy.$http.put('/prizrak/httpClientConfig', config); 36 | } 37 | 38 | export default function createPrizrakApi(proxy: any) { 39 | return { 40 | enableProxy: enableProxy(proxy), 41 | disableProxy: disableProxy(proxy), 42 | checkAddressPort: checkAddressPort(proxy), 43 | configDir: configDir(proxy), 44 | exit: exit(proxy), 45 | updateHTTPClientConfig: updateHTTPClientConfig(proxy), 46 | } 47 | } -------------------------------------------------------------------------------- /doc/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 | Prizrak-Box 3 |

Prizrak-Box

4 |

一个简易的 Mihomo 桌面客户端

5 |
6 | 7 | ## 下载地址 8 | 9 | [下载APP](https://github.com/legiz-ru/Prizrak-Box/releases) 10 | 11 | ## 功能特点 12 | 13 | - 支持本地 HTTP/HTTPS/SOCKS 代理 14 | - 支持 Vmess, Vless, Shadowsocks, Trojan, Tuic, Hysteria, Hysteria2, Wireguard, Mieru 协议 15 | - 支持分享链接、订阅链接、Base64 格式、Yaml 格式、Json 格式的数据解析 16 | - 内置订阅转换,可将clash、v2ray、sing-box订阅转换为 mihomo 配置 17 | - 对无规则订阅自动添加极简规则分组 18 | - 开启 DNS 覆写可防止 DNS 泄露 19 | - 支持统一所有订阅的规则和分组,支持自定义模版规则和分组 20 | - 支持 TUN 模式 和 Smart 智能分组 21 | 22 | ## Deeplink 导入 23 | 24 | - 打开 `prizrak-box://install-config?url=https://sub.example.com/username` 这样的链接即可导入配置 25 | - deeplink 中的额外参数会被忽略,订阅地址内部的查询参数会被完整保留 26 | 27 | ## 支持的系统平台 28 | 29 | - Windows 10/11 AMD64/ARM64 30 | - macOS 11.0+ AMD64/ARM64 31 | - Linux AMD64/ARM64 32 | 33 | ## 如何开启 TUN 34 | 35 | - 设置 → 开启授权 → 重启软件 → 弹出授权框 → 完成授权 36 | - 进入软件后即可开启 TUN 模式 37 | 38 | ## 提示 Px 需要网络接入 39 | 40 | - 点击 “允许” 即可 41 | 42 | ## macOS 常见问题汇总 43 | 44 | - [mac.md](mac/mac.md) 45 | 46 | ## 新版主要改进 47 | 48 | 1. 界面改版:支持背景切换、语言切换、拖拽导入 49 | 2. 顶部搜索当前配置节点,快速切换 50 | 3. 增加最小化到托盘功能 51 | 4. 统一规则模板:简约分组、多国别分组、全分组 52 | 5. 暂未迁移 v0.2 版本的爬取模块、导入导出模块 53 | 54 | ## Todo 未来计划 55 | 56 | - 爬取模块 57 | - 导入导出模块 58 | - Bug 修复 59 | 60 | ## 预览 61 | 62 | | 页面 | 界面预览 | 63 | |----|-------------------------------| 64 | | 首页 | ![General](img/home.jpg) | 65 | | 设置 | ![Setting](img/setting.jpg) | 66 | | 代理 | ![Proxies](img/proxies.jpg) | 67 | | 订阅 | ![Profiles](img/profiles.jpg) | 68 | -------------------------------------------------------------------------------- /src-go/pkg/utils/mypool.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/panjf2000/ants/v2" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultJobQueueLength = 32 // 默认任务队列长度 11 | ) 12 | 13 | type Job func(chan struct{}) 14 | 15 | type TimeoutPool struct { 16 | antPool *ants.Pool 17 | wg sync.WaitGroup 18 | } 19 | 20 | // NewTimeoutPoolWithDefaults 初始化一个任务队列长度32 21 | func NewTimeoutPoolWithDefaults() *TimeoutPool { 22 | p, _ := ants.NewPool(defaultJobQueueLength, func(opts *ants.Options) { 23 | opts.PreAlloc = true 24 | }) 25 | return &TimeoutPool{p, sync.WaitGroup{}} 26 | } 27 | 28 | // NewTimeoutPool 初始化一个任务队列长度为size 29 | func NewTimeoutPool(size int) *TimeoutPool { 30 | p, _ := ants.NewPool(size, func(opts *ants.Options) { 31 | opts.PreAlloc = true 32 | }) 33 | return &TimeoutPool{p, sync.WaitGroup{}} 34 | } 35 | 36 | // SubmitWithTimeout 提交一个任务到协程池 37 | func (p *TimeoutPool) SubmitWithTimeout(job Job, timeout time.Duration) { 38 | _ = p.antPool.Submit(func() { 39 | done := make(chan struct{}, 1) 40 | go job(done) 41 | select { 42 | case <-done: 43 | case <-time.After(timeout): 44 | } 45 | p.wg.Done() 46 | }) 47 | } 48 | 49 | // Submit 提交一个任务到协程池 50 | func (p *TimeoutPool) Submit(job Job) { 51 | _ = p.antPool.Submit(func() { 52 | done := make(chan struct{}, 1) 53 | go job(done) 54 | select { 55 | case <-done: 56 | } 57 | p.wg.Done() 58 | }) 59 | } 60 | 61 | // StartAndWait 启动并等待协程池内的运行全部运行结束 62 | func (p *TimeoutPool) StartAndWait() { 63 | p.wg.Wait() 64 | p.antPool.Release() 65 | } 66 | 67 | func (p *TimeoutPool) WaitCount(count int) { 68 | p.wg.Add(count) 69 | } 70 | -------------------------------------------------------------------------------- /src-go/api/handlers/mihomo.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/go-chi/render" 6 | "github.com/legiz-ru/prizrak-box/api/models" 7 | "github.com/legiz-ru/prizrak-box/pkg/cache" 8 | "github.com/legiz-ru/prizrak-box/pkg/constant" 9 | sys "github.com/legiz-ru/prizrak-box/pkg/sys/admin" 10 | "net/http" 11 | ) 12 | 13 | func Mihomo(r chi.Router) { 14 | r.Mount("/mihomo", MihomoRouter()) 15 | } 16 | 17 | func MihomoRouter() chi.Router { 18 | r := chi.NewRouter() 19 | 20 | r.Get("/", getMihomo) 21 | r.Put("/", updateMihomo) 22 | 23 | r.Get("/admin", getAdmin) 24 | 25 | return r 26 | } 27 | 28 | func getMihomo(w http.ResponseWriter, r *http.Request) { 29 | var mi models.Mihomo 30 | _ = cache.Get(constant.Mihomo, &mi) 31 | 32 | if mi.BindAddress == "" { 33 | mi = models.Mihomo{ 34 | Mode: "rule", 35 | Proxy: false, 36 | Tun: false, 37 | Port: 9697, 38 | BindAddress: "127.0.0.1", 39 | Stack: "Mixed", 40 | Dns: false, 41 | Ipv6: false, 42 | } 43 | _ = cache.Put(constant.Mihomo, mi) 44 | } 45 | 46 | render.JSON(w, r, mi) 47 | } 48 | 49 | func updateMihomo(w http.ResponseWriter, r *http.Request) { 50 | // 读取请求体 51 | var mi models.Mihomo 52 | if err := render.DecodeJSON(r.Body, &mi); err != nil { 53 | ErrorResponse(w, r, err) 54 | return 55 | } 56 | 57 | _ = cache.Put(constant.Mihomo, mi) 58 | 59 | render.NoContent(w, r) 60 | } 61 | 62 | func getAdmin(w http.ResponseWriter, r *http.Request) { 63 | admin := struct { 64 | Data bool `json:"data"` 65 | }{sys.IsAdmin()} 66 | 67 | render.JSON(w, r, admin) 68 | } 69 | -------------------------------------------------------------------------------- /src/util/version.ts: -------------------------------------------------------------------------------- 1 | export function normalizeVersion(version?: string | null): string { 2 | if (!version) { 3 | return ''; 4 | } 5 | 6 | const trimmed = version.trim(); 7 | if (!trimmed) { 8 | return ''; 9 | } 10 | 11 | const withoutPrefix = trimmed.replace(/^v/i, ''); 12 | const core = withoutPrefix.split(/[+\-]/)[0]; 13 | return core.trim(); 14 | } 15 | 16 | export function compareVersions(a?: string | null, b?: string | null): number { 17 | const left = normalizeVersion(a); 18 | const right = normalizeVersion(b); 19 | 20 | if (!left && !right) { 21 | return 0; 22 | } 23 | if (!left) { 24 | return -1; 25 | } 26 | if (!right) { 27 | return 1; 28 | } 29 | 30 | const leftParts = left.split('.').map((segment) => parseInt(segment, 10) || 0); 31 | const rightParts = right.split('.').map((segment) => parseInt(segment, 10) || 0); 32 | const length = Math.max(leftParts.length, rightParts.length); 33 | 34 | for (let index = 0; index < length; index += 1) { 35 | const leftValue = leftParts[index] ?? 0; 36 | const rightValue = rightParts[index] ?? 0; 37 | if (leftValue > rightValue) { 38 | return 1; 39 | } 40 | if (leftValue < rightValue) { 41 | return -1; 42 | } 43 | } 44 | 45 | return 0; 46 | } 47 | 48 | export function resolveVersionLabel(tag?: string | null, name?: string | null): string { 49 | if (name && name.trim()) { 50 | return name.trim(); 51 | } 52 | 53 | if (tag && tag.trim()) { 54 | return tag.trim(); 55 | } 56 | 57 | return ''; 58 | } 59 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import Icons from 'unplugin-icons/vite'; 4 | import IconsResolver from "unplugin-icons/resolver"; 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import {ElementPlusResolver} from 'unplugin-vue-components/resolvers' 8 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' 9 | import path from 'path' 10 | 11 | const pathSrc = path.resolve(__dirname, 'src') 12 | 13 | // https://vitejs.dev/config/ 14 | export default defineConfig({ 15 | resolve: { 16 | alias: { 17 | '@': pathSrc, 18 | }, 19 | }, 20 | plugins: [vue(), 21 | AutoImport({ 22 | imports: ["vue"], 23 | resolvers: [ 24 | ElementPlusResolver(), 25 | IconsResolver({ 26 | prefix: "Icon", 27 | }), 28 | ], 29 | dts: path.resolve(pathSrc, "auto-imports.d.ts"), 30 | }), 31 | Components({ 32 | resolvers: [ 33 | IconsResolver({ 34 | prefix: 'icon', 35 | enabledCollections: ["ep", "mdi"], 36 | }), 37 | ElementPlusResolver() 38 | ], 39 | dts: path.resolve(pathSrc, 'components.d.ts'), 40 | }), 41 | Icons({ 42 | autoInstall: true, 43 | compiler: "vue3", 44 | }), 45 | VueI18nPlugin({ 46 | include: [path.resolve(pathSrc, './locales/**')], 47 | }), 48 | ], 49 | clearScreen: false 50 | }) 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | .idea 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # TypeScript cache 44 | *.tsbuildinfo 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # Webpack 87 | .webpack/ 88 | 89 | # Vite 90 | .vite/ 91 | 92 | # Electron-Forge 93 | out/ 94 | 95 | src-go/px 96 | src-go/prizrak-box 97 | -------------------------------------------------------------------------------- /src/components/setting/MyTun.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 43 | 44 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | html,body { 2 | margin: 0; 3 | padding: 0; 4 | letter-spacing: normal; /* 统一字母间距 */ 5 | line-height: 1; /* 统一行高 */ 6 | background: transparent; 7 | user-select: none; 8 | -webkit-user-select: none; /* 针对WebKit浏览器 */ 9 | -moz-user-select: none; /* 针对Firefox */ 10 | -ms-user-select: none; /* 针对IE浏览器 */ 11 | font-family: 'Twemoji', "Nunito", 'Microsoft YaHei', '微软雅黑', 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 12 | Arial, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 13 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 14 | sans-serif; 15 | height: 100%; 16 | overflow: hidden; 17 | } 18 | 19 | input { 20 | text-transform: none; 21 | } 22 | 23 | 24 | @font-face { 25 | font-family: "Nunito"; 26 | font-style: normal; 27 | src: url("../assets/fonts/nunito-v32-cyrillic_latin-regular.woff2") format("woff2"); 28 | } 29 | 30 | @font-face { 31 | font-family: "Twemoji"; 32 | font-style: normal; 33 | font-display: swap; 34 | src: url("../assets/fonts/TwemojiCountryFlags.woff2") format("woff2"); 35 | unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, 36 | U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F; 37 | } 38 | 39 | .toLow { 40 | color: #39ff14; 41 | padding-top: 2px; 42 | filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.7)); 43 | } 44 | 45 | .toMiddle { 46 | color: #ffd700; 47 | padding-top: 2px; 48 | filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.7)); 49 | } 50 | 51 | .toHigh { 52 | color: #ff4500; 53 | padding-top: 2px; 54 | filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.7)); 55 | } -------------------------------------------------------------------------------- /src/util/ws.ts: -------------------------------------------------------------------------------- 1 | export class WS { 2 | url: string; 3 | ws: WebSocket; 4 | closure: Function; 5 | send: Function; 6 | 7 | constructor( 8 | url: string, 9 | onopen: ((ev: Event) => any) | null = null, 10 | onmessage: ((ev: MessageEvent) => any) | null = null, 11 | onerror: ((ev: Event) => any) | null = null, 12 | onclose: ((ev: CloseEvent) => any) | null = null 13 | ) { 14 | this.url = url; 15 | this.ws = new WebSocket(url); 16 | this.closure = (): void => { 17 | this.ws.close(); 18 | }; 19 | this.send = (msg: any): void => { 20 | this.ws.send(msg); 21 | }; 22 | 23 | // 绑定事件 24 | this.ws.onopen = (ev: Event) => { 25 | onopen?.(ev); 26 | console.log(`websocket ${this.url} 连接开启!`); 27 | }; 28 | 29 | this.ws.onmessage = (ev: MessageEvent) => { 30 | onmessage?.(ev); 31 | }; 32 | 33 | this.ws.onerror = (ev: Event) => { 34 | onerror?.(ev); 35 | console.log(`websocket ${this.url} 连接发生错误:`, ev); 36 | }; 37 | 38 | this.ws.onclose = (ev: CloseEvent) => { 39 | onclose?.(ev); 40 | console.log(`websocket ${this.url} 连接关闭!`); 41 | this.ws.onmessage = null; // 清除监听 42 | }; 43 | } 44 | 45 | // 强制清理 46 | close() { 47 | if ( 48 | this.ws.readyState === WebSocket.OPEN || 49 | this.ws.readyState === WebSocket.CONNECTING 50 | ) { 51 | this.ws.close(); 52 | } 53 | this.ws.onmessage = null; // 移除监听 54 | this.ws.onerror = null; 55 | this.ws.onclose = null; 56 | console.log("WebSocket 已完全清理"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/api/profiles/index.ts: -------------------------------------------------------------------------------- 1 | import {AxiosRequestConfig} from "axios"; 2 | import {Profile} from "@/types/profile"; 3 | 4 | // 添加配置从input 5 | const addProfileFromInput = (proxy: any) => async function (profile: Profile, config?: AxiosRequestConfig): Promise { 6 | return await proxy.$http.post('/profile', profile, config); 7 | } 8 | 9 | // 添加配置从文件 10 | const addProfileFromFile = (proxy: any) => async function (profile: Profile) { 11 | return await proxy.$http.post('/profile/file', profile); 12 | } 13 | 14 | // 删除配置 15 | const deleteProfile = (proxy: any) => async function (profile: Profile) { 16 | return await proxy.$http.post('/profile/delete', profile); 17 | } 18 | 19 | // 修改配置 20 | const updateProfile = (proxy: any) => async function (profile: Profile) { 21 | return await proxy.$http.put('/profile', profile); 22 | } 23 | 24 | // 获取配置列表 25 | const getProfileList = (proxy: any) => async function (): Promise { 26 | return await proxy.$http.get('/profile'); 27 | } 28 | 29 | // 刷新配置 30 | const refreshProfile = (proxy: any) => async function (profile: Profile) { 31 | return await proxy.$http.put('/profile/refresh', profile); 32 | } 33 | 34 | // 切换配置 35 | const switchProfile = (proxy: any) => async function (profile: Profile) { 36 | return await proxy.$http.patch('/profile', profile); 37 | } 38 | 39 | 40 | export default function createProfilesApi(proxy: any) { 41 | return { 42 | addProfileFromInput: addProfileFromInput(proxy), 43 | addProfileFromFile: addProfileFromFile(proxy), 44 | deleteProfile: deleteProfile(proxy), 45 | updateProfile: updateProfile(proxy), 46 | getProfileList: getProfileList(proxy), 47 | refreshProfile: refreshProfile(proxy), 48 | switchProfile: switchProfile(proxy), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src-go/pkg/sys/admin/admin_windows.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | sys "github.com/legiz-ru/prizrak-box/pkg/sys/cmd" 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | // IsAdmin 检查程序是否具有管理员权限 12 | func IsAdmin() bool { 13 | var sid *windows.SID 14 | 15 | // 分配并初始化管理员组的 SID 16 | err := windows.AllocateAndInitializeSid( 17 | &windows.SECURITY_NT_AUTHORITY, 18 | 2, 19 | windows.SECURITY_BUILTIN_DOMAIN_RID, 20 | windows.DOMAIN_ALIAS_RID_ADMINS, 21 | 0, 0, 0, 0, 0, 0, 22 | &sid) 23 | if err != nil { 24 | // 如果 SID 初始化失败,没有管理员权限 25 | return false 26 | } 27 | defer windows.FreeSid(sid) // 确保释放 SID 资源 28 | 29 | // 检查当前进程是否属于管理员组 30 | isMember, err := windows.Token(0).IsMember(sid) 31 | if err != nil { 32 | // 如果检查失败,没有管理员权限 33 | return false 34 | } 35 | 36 | return isMember 37 | } 38 | 39 | // KillProcessesByName 杀死所有名字为指定名称的进程 40 | func KillProcessesByName(name string) error { 41 | // 将目标进程名称转换为小写以便精确匹配 42 | name = strings.ToLower(name) 43 | 44 | // 使用 tasklist 命令查找所有进程 45 | output, err := sys.Command("tasklist") 46 | if err != nil { 47 | return fmt.Errorf("failed to execute tasklist: %w", err) 48 | } 49 | 50 | // 按行解析 tasklist 输出 51 | lines := strings.Split(output, "\n") 52 | for _, line := range lines { 53 | // 将每行转换为小写以便匹配 54 | lineLower := strings.ToLower(line) 55 | 56 | // 检查是否包含目标进程名称 57 | if strings.HasPrefix(lineLower, name+" ") || strings.Contains(lineLower, " "+name+" ") { 58 | fields := strings.Fields(line) 59 | if len(fields) > 1 { 60 | pid := fields[1] // 获取进程 ID 61 | 62 | // 使用 taskkill 命令杀死进程 63 | _, err = sys.Command("taskkill", "/F", "/PID", pid) 64 | if err != nil { 65 | return fmt.Errorf("failed to kill process %s (PID: %s): %w", name, pid, err) 66 | } 67 | } 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /src/components/MyLayout.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | 38 | -------------------------------------------------------------------------------- /src-go/api/job/alive.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "github.com/metacubex/mihomo/hub/executor" 6 | "github.com/metacubex/mihomo/log" 7 | "github.com/legiz-ru/prizrak-box/pkg/cache" 8 | "github.com/legiz-ru/prizrak-box/pkg/cron" 9 | sys "github.com/legiz-ru/prizrak-box/pkg/sys/proxy" 10 | "github.com/legiz-ru/prizrak-box/pkg/utils" 11 | "io" 12 | "net/http" 13 | "os" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | var aliveLock sync.Mutex 19 | 20 | func AliveJob(name string, server string) { 21 | // 创建 HTTP 客户端 22 | client := &http.Client{ 23 | Transport: &http.Transport{ 24 | MaxIdleConns: 5, // 最大空闲连接数 25 | IdleConnTimeout: 30 * time.Second, // 空闲连接超时时间 26 | DisableKeepAlives: false, // 启用 Keep-Alive 27 | }, 28 | } 29 | 30 | cron.AddTask(name, 3*time.Second, func() { 31 | if aliveLock.TryLock() { 32 | defer aliveLock.Unlock() 33 | } else { 34 | return 35 | } 36 | 37 | // 请求地址 38 | url := fmt.Sprintf("http://%s/pxAlive", server) 39 | 40 | // 创建请求 41 | req, err := http.NewRequest("GET", url, nil) 42 | if err != nil { 43 | return 44 | } 45 | 46 | // 设置 Keep-Alive 头 47 | req.Header.Set("Connection", "keep-alive") 48 | 49 | // 发送请求 50 | resp, err := client.Do(req) 51 | if err != nil { 52 | log.Infoln("[1]检测到父进程退出,准备退出...") 53 | Exit(true) 54 | return 55 | } 56 | defer func(Body io.ReadCloser) { 57 | _ = Body.Close() 58 | }(resp.Body) 59 | 60 | body, err := io.ReadAll(resp.Body) 61 | if err != nil { 62 | log.Infoln("[2]检测到父进程退出,准备退出...") 63 | Exit(true) 64 | return 65 | } 66 | 67 | if string(body) != "alive" { 68 | log.Infoln("[3]检测到父进程退出,准备退出...") 69 | Exit(true) 70 | } 71 | }) 72 | } 73 | 74 | func Exit(needExit bool) { 75 | cache.Close() 76 | utils.UnlockSingleton() 77 | executor.Shutdown() 78 | sys.DisableProxy() 79 | if needExit { 80 | os.Exit(0) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/store/settingStore.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import {defaultPersist} from "@/types/persist"; 3 | 4 | export const useSettingStore = defineStore('setting', { 5 | state: () => ({ 6 | testUrl: 'https://www.google.com/blank.html', 7 | port: 12345, 8 | bindAddress: "127.0.0.1", 9 | stack: 'Mixed', 10 | ipv6: false, 11 | dns: false, 12 | startup: false, 13 | auth: false, 14 | hwid: true, 15 | hwidHeaders: { 16 | hwid: '', 17 | os: '', 18 | osVersion: '', 19 | model: '', 20 | }, 21 | }), 22 | actions: { 23 | setTestUrl(testUrl: any) { 24 | this.testUrl = testUrl; 25 | }, 26 | setPort(port: any) { 27 | this.port = Number(port); 28 | }, 29 | setStack(stack: any) { 30 | this.stack = stack; 31 | }, 32 | setIpv6(ipv6: any) { 33 | this.ipv6 = ipv6; 34 | }, 35 | setDns(dns: any) { 36 | this.dns = dns; 37 | }, 38 | setStartup(startup: any) { 39 | this.startup = startup; 40 | }, 41 | setBindAddress(bindAddress: any) { 42 | this.bindAddress = bindAddress; 43 | }, 44 | setAuth(auth: any) { 45 | this.auth = auth; 46 | }, 47 | setHwid(hwid: any) { 48 | this.hwid = hwid; 49 | }, 50 | setHwidHeaders(headers: { hwid?: string; os?: string; osVersion?: string; model?: string }) { 51 | this.hwidHeaders = { 52 | hwid: headers?.hwid ?? this.hwidHeaders.hwid, 53 | os: headers?.os ?? this.hwidHeaders.os, 54 | osVersion: headers?.osVersion ?? this.hwidHeaders.osVersion, 55 | model: headers?.model ?? this.hwidHeaders.model, 56 | }; 57 | }, 58 | }, 59 | persist: defaultPersist, 60 | }); 61 | -------------------------------------------------------------------------------- /src-electron/preload.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import {clipboard, contextBridge, ipcRenderer, shell} from 'electron'; 4 | import os from 'os'; 5 | 6 | // tray相关 7 | contextBridge.exposeInMainWorld('pxTray', { 8 | on: (name, callback) => { 9 | const eventName = 'px_' + name; 10 | // 移除旧监听器,确保只注册一次 11 | ipcRenderer.removeAllListeners(eventName); 12 | ipcRenderer.on(eventName, (_event, ...value) => callback(...value)); 13 | }, 14 | emit: (name, ...value) => ipcRenderer.send('px_' + name, ...value) 15 | }); 16 | 17 | 18 | // 深度链接相关 19 | contextBridge.exposeInMainWorld('pxDeepLink', { 20 | onImportProfile: (callback) => { 21 | ipcRenderer.removeAllListeners('import-profile-from-deeplink'); 22 | ipcRenderer.on('import-profile-from-deeplink', (_event, data) => callback(data)); 23 | }, 24 | notifyReady: () => { 25 | ipcRenderer.send('deeplink-handler-ready'); 26 | } 27 | }); 28 | 29 | 30 | // 缓存接口 31 | contextBridge.exposeInMainWorld('pxStore', { 32 | get: (key) => ipcRenderer.invoke('store:get', key), 33 | set: (key, value) => ipcRenderer.invoke('store:set', key, value) 34 | }); 35 | 36 | // 获取系统信息 37 | contextBridge.exposeInMainWorld('pxOs', () => { 38 | switch (os.type()) { 39 | case 'Darwin': 40 | return "MacOS " + os.arch() 41 | case 'Linux': 42 | return "Linux " + os.arch() 43 | case 'Windows_NT': 44 | return "Windows " + os.arch() 45 | default: 46 | return "Unknown"; 47 | } 48 | }); 49 | 50 | // 打开配置目录 51 | contextBridge.exposeInMainWorld('pxConfigDir', (url: string) => shell.openPath(url)); 52 | 53 | // 获取剪贴板内容 54 | contextBridge.exposeInMainWorld('pxClipboard', () => clipboard.readText()); 55 | 56 | // 打开外部URL地址 57 | contextBridge.exposeInMainWorld('pxOpen', (url: string) => shell.openExternal(url)); 58 | 59 | // 控制标题栏 60 | if (process.platform !== 'darwin') { 61 | contextBridge.exposeInMainWorld('pxShowBar', () => console.log('pxShowBar')); 62 | } -------------------------------------------------------------------------------- /src-go/pkg/utils/device_details_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package utils 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func collectDeviceDetails() DeviceDetails { 13 | details := DeviceDetails{} 14 | 15 | if hwid := readFirstExistingFile( 16 | "/etc/machine-id", 17 | "/var/lib/dbus/machine-id", 18 | ); hwid != "" { 19 | details.HWID = hwid 20 | } 21 | 22 | if pretty := readKeyFromFile("/etc/os-release", "PRETTY_NAME"); pretty != "" { 23 | details.OS = pretty 24 | } else { 25 | details.OS = "Linux" 26 | } 27 | 28 | var uts unix.Utsname 29 | if err := unix.Uname(&uts); err == nil { 30 | details.OSVersion = trimCString(uts.Release[:]) 31 | } 32 | 33 | details.Model = readFirstExistingFile( 34 | "/sys/devices/virtual/dmi/id/product_name", 35 | "/sys/devices/virtual/dmi/id/board_name", 36 | "/sys/class/dmi/id/product_name", 37 | ) 38 | 39 | if details.Model == "" { 40 | details.Model = readFirstExistingFile( 41 | "/sys/devices/virtual/dmi/id/product_version", 42 | "/sys/class/dmi/id/product_version", 43 | ) 44 | } 45 | 46 | return details 47 | } 48 | 49 | func readFirstExistingFile(paths ...string) string { 50 | for _, path := range paths { 51 | data, err := os.ReadFile(path) 52 | if err == nil { 53 | trimmed := strings.TrimSpace(string(data)) 54 | if trimmed != "" { 55 | return trimmed 56 | } 57 | } 58 | } 59 | return "" 60 | } 61 | 62 | func readKeyFromFile(path, key string) string { 63 | data, err := os.ReadFile(path) 64 | if err != nil { 65 | return "" 66 | } 67 | lines := strings.Split(string(data), "") 68 | prefix := key + "=" 69 | for _, line := range lines { 70 | if strings.HasPrefix(line, prefix) { 71 | value := strings.TrimPrefix(line, prefix) 72 | return strings.Trim(value, `"`) 73 | } 74 | } 75 | return "" 76 | } 77 | 78 | func trimCString(buf []byte) string { 79 | n := 0 80 | for ; n < len(buf); n++ { 81 | if buf[n] == 0 { 82 | break 83 | } 84 | } 85 | return strings.TrimSpace(string(buf[:n])) 86 | } 87 | -------------------------------------------------------------------------------- /src/components/MyHr.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 76 | 77 | 85 | -------------------------------------------------------------------------------- /src/store/webStore.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | 3 | export interface CustomDashboard { 4 | name: string; 5 | url: string; 6 | } 7 | 8 | export const useWebStore = defineStore('web', { 9 | state: () => ({ 10 | host: '127.0.0.1', // 默认值 11 | port: '9686', // 默认端口 12 | secret: 'Y8IUaPeFLTRvsrdf2mUJkLMBuphVZRE5', // 默认密钥 13 | logs: [], // 日志 14 | dnd: false, // 拖拽显示 15 | dProfile: [], // 传输文件 拖拽添加文件用 16 | fProfile: {}, // 更新profile 配置切换用 17 | customDashboards: [] as CustomDashboard[], 18 | }), 19 | getters: { 20 | // 确保使用 state 参数引用正确 21 | baseUrl: (state) => `http://${state.host}:${state.port}`, 22 | wsUrl: (state) => `ws://${state.host}:${state.port}`, 23 | }, 24 | actions: { 25 | setHost(host: string) { 26 | if (host) this.host = host; 27 | }, 28 | setPort(port: string) { 29 | if (port) this.port = port; 30 | }, 31 | setSecret(secret: string) { 32 | if (secret) this.secret = secret; 33 | }, 34 | addLog(log: any) { 35 | // 只保留最近的100条日志 36 | if (this.logs.length >= 100) { 37 | this.logs.pop(); 38 | } 39 | // 在头部添加新日志 40 | this.logs.unshift(log); 41 | }, 42 | addCustomDashboard(dashboard: CustomDashboard) { 43 | this.customDashboards.push(dashboard); 44 | }, 45 | updateCustomDashboard(index: number, dashboard: CustomDashboard) { 46 | if (index < 0 || index >= this.customDashboards.length) { 47 | return; 48 | } 49 | 50 | this.customDashboards.splice(index, 1, dashboard); 51 | }, 52 | removeCustomDashboard(index: number) { 53 | if (index < 0 || index >= this.customDashboards.length) { 54 | return; 55 | } 56 | 57 | this.customDashboards.splice(index, 1); 58 | }, 59 | }, 60 | persist: true, 61 | }); 62 | -------------------------------------------------------------------------------- /src-go/pkg/utils/snowflake.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | const ( 9 | epoch = int64(1577808000000) // 设置起始时间(时间戳/毫秒):2020-01-01 00:00:00,有效期69年 10 | timestampBits = uint(41) // 时间戳占用位数 11 | datacenteridBits = uint(5) // 数据中心id所占位数 12 | workeridBits = uint(5) // 机器id所占位数 13 | sequenceBits = uint(12) // 序列所占的位数 14 | timestampMax = int64(-1 ^ (-1 << timestampBits)) // 时间戳最大值 15 | sequenceMask = int64(-1 ^ (-1 << sequenceBits)) // 支持的最大序列id数量 16 | workeridShift = sequenceBits // 机器id左移位数 17 | datacenteridShift = sequenceBits + workeridBits // 数据中心id左移位数 18 | timestampShift = sequenceBits + workeridBits + datacenteridBits // 时间戳左移位数 19 | ) 20 | 21 | type snowflake struct { 22 | sync.Mutex 23 | timestamp int64 24 | workerid int64 25 | datacenterid int64 26 | sequence int64 27 | } 28 | 29 | var s = &snowflake{ 30 | timestamp: 0, 31 | datacenterid: 1, 32 | workerid: 2, 33 | sequence: 0, 34 | } 35 | 36 | func SnowflakeId() int64 { 37 | return s.nextVal() 38 | } 39 | 40 | // nextVal 返回下一个Snowflake ID 41 | func (s *snowflake) nextVal() int64 { 42 | s.Lock() 43 | now := time.Now().UnixNano() / 1000000 // 转毫秒 44 | if s.timestamp == now { 45 | // 当同一时间戳(精度:毫秒)下多次生成id会增加序列号 46 | s.sequence = (s.sequence + 1) & sequenceMask 47 | if s.sequence == 0 { 48 | // 如果当前序列超出12bit长度,则需要等待下一毫秒 49 | // 下一毫秒将使用sequence:0 50 | for now <= s.timestamp { 51 | now = time.Now().UnixNano() / 1000000 52 | } 53 | } 54 | } else { 55 | // 不同时间戳(精度:毫秒)下直接使用序列号:0 56 | s.sequence = 0 57 | } 58 | t := now - epoch 59 | if t > timestampMax { 60 | s.Unlock() 61 | return 0 62 | } 63 | s.timestamp = now 64 | r := (t)<= 0; i--) { 33 | result += arr[i]; 34 | if (i > 0) { 35 | result += separator; 36 | } 37 | } 38 | return result; 39 | } 40 | 41 | // 将 Date 对象格式化为 "YYYY-MM-DD HH:mm:ss.SSS" 的字符串 42 | export function formatDate(date: Date): string { 43 | const year = date.getFullYear(); 44 | const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从 0 开始,需要加 1 45 | const day = String(date.getDate()).padStart(2, "0"); 46 | const hours = String(date.getHours()).padStart(2, "0"); 47 | const minutes = String(date.getMinutes()).padStart(2, "0"); 48 | const seconds = String(date.getSeconds()).padStart(2, "0"); 49 | const milliseconds = String(date.getMilliseconds()).padStart(3, "0"); // 毫秒部分 50 | 51 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; 52 | } 53 | 54 | // 校验是否 url 格式 55 | export function isHttpOrHttps(url: string): boolean { 56 | if (url.trim() === '') return false; 57 | try { 58 | const parsed = new URL(url); 59 | return parsed.protocol === 'http:' || parsed.protocol === 'https:'; 60 | } catch { 61 | return false; 62 | } 63 | } 64 | 65 | const innerTemplate = ['m0', 'm1', 'm2', 'm3'] 66 | 67 | // 获取模版标题 68 | export function getTemplateTitle(t: any, title: string) { 69 | if (innerTemplate.indexOf(title) > -1) { 70 | return t("rule.group." + title) 71 | } 72 | 73 | return title 74 | } 75 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; 2 | 3 | import Home from '@/views/Home.vue'; 4 | import Setting from '@/views/Setting.vue'; 5 | import Proxies from '@/views/Proxies.vue'; 6 | import Profiles from '@/views/Profiles.vue'; 7 | import Rule from '@/views/Rule.vue'; 8 | import Now from '@/views/rule/Now.vue'; 9 | import Group from '@/views/rule/Group.vue'; 10 | import Ignore from '@/views/rule/Ignore.vue'; 11 | import Connection from '@/views/Connection.vue'; 12 | import Log from '@/views/Log.vue'; 13 | import Crawl from '@/views/Crawl.vue'; 14 | import Dns from '@/views/setting/Dns.vue'; 15 | 16 | const routes: Array = [ 17 | { 18 | path: '/', 19 | name: 'Start', 20 | component: Home, 21 | }, 22 | { 23 | path: '/Home', 24 | name: 'Home', 25 | component: Home, 26 | }, 27 | { 28 | path: '/Setting', 29 | name: 'Setting', 30 | component: Setting, 31 | }, 32 | { 33 | path: '/Setting/Dns', 34 | name: 'Dns', 35 | component: Dns, 36 | }, 37 | { 38 | path: '/Proxies', 39 | name: 'Proxies', 40 | component: Proxies, 41 | }, 42 | { 43 | path: '/Profiles', 44 | name: 'Profiles', 45 | component: Profiles, 46 | }, 47 | { 48 | path: '/Rule', 49 | name: 'Rule', 50 | component: Rule, 51 | children: [ 52 | { 53 | path: 'Now', 54 | name: 'Now', 55 | component: Now, 56 | }, 57 | { 58 | path: 'Group', 59 | name: 'Group', 60 | component: Group, 61 | }, 62 | { 63 | path: 'Ignore', 64 | name: 'Ignore', 65 | component: Ignore, 66 | }, 67 | ], 68 | }, 69 | { 70 | path: '/Connection', 71 | name: 'Connection', 72 | component: Connection, 73 | }, 74 | { 75 | path: '/Log', 76 | name: 'Log', 77 | component: Log, 78 | }, 79 | { 80 | path: '/Crawl', 81 | name: 'Crawl', 82 | component: Crawl, 83 | }, 84 | ]; 85 | 86 | const router = createRouter({ 87 | history: createWebHistory(), 88 | routes, 89 | }); 90 | 91 | export default router; -------------------------------------------------------------------------------- /src/components/menu/MyRule.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 67 | 68 | 94 | -------------------------------------------------------------------------------- /src-go/pkg/utils/randstr.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "encoding/hex" 9 | ) 10 | 11 | // RandBytes generates n random bytes 12 | func RandBytes(n int) []byte { 13 | b := make([]byte, n) 14 | _, err := rand.Read(b) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return b 19 | } 20 | 21 | const Base64Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/" 22 | const Base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 23 | const HexChars = "0123456789abcdef" 24 | const DecChars = "0123456789" 25 | 26 | // RandBase64 generates a random Base64 string with length of n 27 | // 28 | // Example: X02+jDDF/exDoqPg9/aXlzbUCN93GIQ5 29 | func RandBase64(n int) string { return RandString(n, Base64Chars) } 30 | 31 | // RandBase62 generates a random Base62 string with length of n 32 | // 33 | // Example: 1BsNqB61o4ztSqLC6labKGNf4MYy352X 34 | func RandBase62(n int) string { return RandString(n, Base62Chars) } 35 | 36 | // RandDec generates a random decimal number string with length of n 37 | // 38 | // Example: 37110235710860781655802098192113 39 | func RandDec(n int) string { return RandString(n, DecChars) } 40 | 41 | // RandHex generates a random Hexadecimal string with length of n 42 | // 43 | // Example: 67aab2d956bd7cc621af22cfb169cba8 44 | func RandHex(n int) string { return RandString(n, HexChars) } 45 | 46 | // list of default letters that can be used to make a random string when calling String 47 | // function with no letters provided 48 | var defLetters = []rune(Base62Chars) 49 | 50 | // RandString generates a random string using only letters provided in the letters parameter. 51 | // 52 | // If user omits letters parameter, this function will use Base62Chars instead. 53 | func RandString(n int, letters ...string) string { 54 | var letterRunes []rune 55 | if len(letters) == 0 { 56 | letterRunes = defLetters 57 | } else { 58 | letterRunes = []rune(letters[0]) 59 | } 60 | 61 | var bb bytes.Buffer 62 | bb.Grow(n) 63 | l := uint32(len(letterRunes)) 64 | // on each loop, generate one random rune and append to output 65 | for i := 0; i < n; i++ { 66 | bb.WriteRune(letterRunes[binary.BigEndian.Uint32(RandBytes(4))%l]) 67 | } 68 | return bb.String() 69 | } 70 | 71 | // MD5 计算md5 72 | func MD5(v string) string { 73 | d := []byte(v) 74 | m := md5.New() 75 | m.Write(d) 76 | return hex.EncodeToString(m.Sum(nil)) 77 | } 78 | -------------------------------------------------------------------------------- /doc/README.ru.md: -------------------------------------------------------------------------------- 1 |
2 | Prizrak-Box 3 |

Prizrak-Box

4 |

Простой настольный клиент Mihomo

5 |
6 | 7 | ## Ссылка на скачивание 8 | 9 | [Скачать приложение](https://github.com/legiz-ru/Prizrak-Box/releases) 10 | 11 | ## Основные функции 12 | 13 | - Поддержка локального HTTP/HTTPS/SOCKS прокси 14 | - Поддержка протоколов: Vmess, Vless, Shadowsocks, Trojan, Tuic, Hysteria, Hysteria2, Wireguard, Mieru 15 | - Поддержка обмена ссылками, подписками, форматов Base64 и Yaml 16 | - Встроенная конвертация подписок для создания конфигурации Mihomo 17 | - Автоматическое добавление минимальных групп правил для неподготовленных подписок 18 | - Включение перезаписи DNS для предотвращения утечки DNS 19 | - Поддержка унификации правил и групп для всех подписок 20 | - Поддержка режима TUN 21 | 22 | ## Импорт через deeplink 23 | 24 | - Профили можно импортировать, открыв ссылку вида `prizrak-box://install-config?url=https://sub.example.com/username` 25 | - Лишние параметры в deeplink игнорируются, при этом параметры внутри адреса подписки сохраняются 26 | 27 | ## Поддерживаемые платформы 28 | 29 | - Windows 10/11 AMD64/ARM64 30 | - macOS 11.0+ AMD64/ARM64 31 | - Linux AMD64/ARM64 32 | 33 | ## Как включить TUN 34 | 35 | - Настройки → Включить авторизацию → Перезапустить приложение → Подтвердить авторизацию в всплывающем окне → Готово 36 | - После входа в приложение можно активировать режим TUN 37 | 38 | ## Запрос Px на сетевой доступ 39 | 40 | - Просто нажмите "Разрешить" 41 | 42 | ## Часто задаваемые вопросы для macOS 43 | 44 | - [mac.md](mac/mac.md) 45 | 46 | ## Основные изменения в новой версии 47 | 48 | 1. Обновленный интерфейс: поддержка смены фона, языка и перетаскивания файлов 49 | 2. Поиск по текущим узлам конфигурации для быстрого переключения 50 | 3. Добавлена возможность минимизации в трей 51 | 4. Унифицированные шаблоны правил: минималистичные группы, национальные группы, полные группы 52 | 5. Модуль парсинга и импорта/экспорта данных из версии v0.2 пока не перенесены 53 | 54 | ## Планируемые задачи 55 | 56 | - Модуль парсинга 57 | - Модуль импорта/экспорта 58 | - Исправление ошибок 59 | 60 | ## Превью 61 | 62 | | Страница | Пример интерфейса | 63 | |-----------|-------------------------------| 64 | | Главная | ![General](img/home.jpg) | 65 | | Настройки | ![Setting](img/setting.jpg) | 66 | | Прокси | ![Proxies](img/proxies.jpg) | 67 | | Подписки | ![Profiles](img/profiles.jpg) | 68 | -------------------------------------------------------------------------------- /src/util/axiosRequest.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosInstance, AxiosRequestConfig, AxiosResponse} from 'axios'; 2 | 3 | // axios的封装 4 | export class AxiosRequest { 5 | 6 | private instance: AxiosInstance; 7 | 8 | // 默认配置baseURL等 9 | constructor(baseURL: string, secret: string = "", timeout = 30000) { 10 | this.instance = axios.create({ 11 | baseURL, 12 | timeout, 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Authorization': 'Bearer ' + secret 16 | } 17 | }) 18 | 19 | // 添加请求拦截器 20 | this.instance.interceptors.request.use( 21 | (config: any) => { 22 | // 在发送请求之前做些什么 23 | return config; 24 | }, 25 | (error: any) => { 26 | // 处理请求错误 27 | // @ts-ignore 28 | return Promise.reject(error); 29 | }, 30 | ); 31 | 32 | // 添加响应拦截器 33 | this.instance.interceptors.response.use( 34 | (response: AxiosResponse) => { 35 | // 对响应数据做处理,只有状态码为 200 或 204 时返回数据 36 | if (response.status === 200) { 37 | return response.data; 38 | } 39 | }, 40 | (e: any) => { 41 | if (e['response'] && e['response']['data']) { 42 | return Promise.reject(e['response']['data']); 43 | } 44 | // 处理响应错误 45 | // @ts-ignore 46 | return Promise.reject(e); 47 | }, 48 | ); 49 | } 50 | 51 | get(url: string, config?: AxiosRequestConfig): Promise> { 52 | return this.instance.get(url, config); 53 | } 54 | 55 | post(url: string, params?: any, config?: AxiosRequestConfig): Promise> { 56 | return this.instance.post(url, params, config); 57 | } 58 | 59 | put(url: string, params?: any, config?: AxiosRequestConfig): Promise> { 60 | return this.instance.put(url, params, config); 61 | } 62 | 63 | patch(url: string, params?: any, config?: AxiosRequestConfig): Promise> { 64 | return this.instance.patch(url, params, config); 65 | } 66 | 67 | delete(url: string, config?: AxiosRequestConfig): Promise> { 68 | return this.instance.delete(url, config); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/util/dashboard.ts: -------------------------------------------------------------------------------- 1 | import type {CustomDashboard} from "@/store/webStore"; 2 | import {isHttpOrHttps} from "@/util/format"; 3 | 4 | export interface DashboardTemplate { 5 | key: string; 6 | name: string; 7 | url: string; 8 | } 9 | 10 | export interface DashboardOption extends DashboardTemplate { 11 | isCustom?: boolean; 12 | } 13 | 14 | export interface DashboardLink { 15 | key: string; 16 | name: string; 17 | url: string; 18 | } 19 | 20 | export const defaultDashboards: DashboardTemplate[] = [ 21 | { 22 | key: "metacubexd", 23 | name: "MetaCubeXD", 24 | url: "https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret", 25 | }, 26 | { 27 | key: "yacd", 28 | name: "Yacd", 29 | url: "https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret", 30 | }, 31 | { 32 | key: "zashboard", 33 | name: "Zashboard", 34 | url: "https://board.zash.run.place/#/setup?http=true&hostname=%host&port=%port&secret=%secret", 35 | }, 36 | ]; 37 | 38 | export const createCustomDashboardOptions = (dashboards: CustomDashboard[]): DashboardOption[] => 39 | dashboards 40 | .map((entry, index) => ({ 41 | key: `custom-${index}`, 42 | name: entry.name?.trim() ?? "", 43 | url: entry.url?.trim() ?? "", 44 | isCustom: true, 45 | })) 46 | .filter((entry) => entry.name !== "" && entry.url !== ""); 47 | 48 | export const resolveDashboardOptions = (customDashboards: CustomDashboard[]): DashboardOption[] => [ 49 | ...defaultDashboards, 50 | ...createCustomDashboardOptions(customDashboards), 51 | ]; 52 | 53 | export const formatDashboardUrl = ( 54 | template: string, 55 | context: { host: string; port: string; secret: string }, 56 | ): string => template 57 | .replace(/%host/g, context.host) 58 | .replace(/%port/g, context.port) 59 | .replace(/%secret/g, context.secret); 60 | 61 | export const createDashboardLinks = ( 62 | customDashboards: CustomDashboard[], 63 | context: { host: string; port: string; secret: string }, 64 | ): DashboardLink[] => 65 | resolveDashboardOptions(customDashboards) 66 | .map((dashboard, index) => ({ 67 | key: dashboard.key || `dashboard-${index}`, 68 | name: dashboard.name, 69 | url: formatDashboardUrl(dashboard.url, context), 70 | })) 71 | .filter((link) => link.name !== "" && link.url !== "" && isHttpOrHttps(link.url)); 72 | -------------------------------------------------------------------------------- /src/components/MySimpleInput.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 54 | 55 | 100 | -------------------------------------------------------------------------------- /src/components/MyTitleBar.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 95 | 96 | 101 | -------------------------------------------------------------------------------- /public/json/theme.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "default", 4 | "bg": "url('/images/default.jpg')" 5 | }, 6 | { 7 | "id": "harbor", 8 | "bg": "url('/images/harbor.jpg')" 9 | }, 10 | { 11 | "id": "gradient", 12 | "bg": [ 13 | "url('/images/gradient1.png')", 14 | "url('/images/gradient2.png')", 15 | "url('/images/gradient3.png')", 16 | "url('/images/gradient4.png')", 17 | "url('/images/gradient5.png')", 18 | "url('/images/gradient6.png')", 19 | "url('/images/gradient7.png')", 20 | "url('/images/gradient8.png')" 21 | ] 22 | }, 23 | { 24 | "id": "sea", 25 | "bg": [ 26 | "url('/images/sea1.jpg')", 27 | "url('/images/sea2.jpg')", 28 | "url('/images/sea3.jpg')", 29 | "url('/images/sea4.jpg')", 30 | "url('/images/sea5.jpg')", 31 | "url('/images/sea6.jpg')" 32 | ] 33 | }, 34 | { 35 | "id": "women", 36 | "bg": [ 37 | "url('/images/woman1.jpg')", 38 | "url('/images/woman2.jpg')", 39 | "url('/images/woman3.jpg')", 40 | "url('/images/woman4.jpg')", 41 | "url('/images/woman5.jpg')", 42 | "url('/images/woman6.png')", 43 | "url('https://cdn.seovx.com/?mom=302')", 44 | "url('https://cdn.seovx.com/ha/?mom=302')" 45 | ] 46 | }, 47 | { 48 | "id": "stars", 49 | "bg": [ 50 | "url('/images/star1.jpg')", 51 | "url('/images/star2.jpg')", 52 | "url('/images/star3.jpg')", 53 | "url('/images/star4.jpg')" 54 | ] 55 | }, 56 | { 57 | "id": "handle", 58 | "bg": [ 59 | "url('/images/shaurma1.jpg')", 60 | "url('/images/shaurma2.jpg')", 61 | "url('/images/shaurma3.jpg')", 62 | "url('/images/shaurma4.jpg')" 63 | ] 64 | }, 65 | { 66 | "id": "comics", 67 | "bg": [ 68 | "https://www.loliapi.com/acg/pc/?a=1", 69 | "https://img.paulzzh.com/touhou/random?a=1", 70 | "https://cdn.seovx.com/d/?mom=302", 71 | "https://img.xjh.me/random_img.php?type=bg&return=302", 72 | "https://www.dmoe.cc/random.php?a=1", 73 | "https://t.alcy.cc/bd?a=1", 74 | "https://api.boxmoe.com/random.php?size=large" 75 | ], 76 | "rand": true 77 | }, 78 | { 79 | "id": "game", 80 | "bg": [ 81 | "https://via.assets.so/game.jpg?w=1920&h=1080" 82 | ], 83 | "rand": true 84 | }, 85 | { 86 | "id": "random", 87 | "bg": [ 88 | "https://bing.img.run/rand.php?a=1", 89 | "https://picsum.photos/1920/1080?a=1", 90 | "https://cdn.seovx.com/?mom=302", 91 | "https://cdn.seovx.com/ha/?mom=302" 92 | ], 93 | "rand": true 94 | }, 95 | { 96 | "id": "custom" 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /src/api/rule/index.ts: -------------------------------------------------------------------------------- 1 | // 获取规则列表 2 | const getRules = (proxy: any) => 3 | async function () { 4 | const data = await proxy.$http.get("/rules"); 5 | return data["rules"]; 6 | }; 7 | 8 | // 获取规则数 9 | const getRuleNum = (proxy: any) => 10 | async function () { 11 | const data = await proxy.$http.get("/rule/num"); 12 | return data["data"]; 13 | }; 14 | 15 | // 获取 ignore 16 | const getIgnore = (proxy: any) => 17 | async function (): Promise { 18 | return await proxy.$http.get("/rule/ignore"); 19 | }; 20 | 21 | // 保存 ignore 22 | const updateIgnore = (proxy: any) => 23 | async function (ignores: any) { 24 | return await proxy.$http.put("/rule/ignore", ignores); 25 | }; 26 | 27 | // 获取 template list 28 | const getTemplateList = (proxy: any) => 29 | async function (): Promise { 30 | return await proxy.$http.get("/rule/list"); 31 | }; 32 | 33 | // 获取 template by id 34 | const getTemplateById = (proxy: any) => 35 | async function (id: string): Promise { 36 | return await proxy.$http.get(`/rule/template/${id}`); 37 | }; 38 | 39 | // 删除 template by id 40 | const deleteTemplateById = (proxy: any) => 41 | async function (id: string) { 42 | return await proxy.$http.delete(`/rule/template/${id}`); 43 | }; 44 | 45 | // 更新 template 46 | const updateTemplate = (proxy: any) => 47 | async function (data: any) { 48 | return await proxy.$http.put(`/rule/template`, data); 49 | }; 50 | 51 | // 创建 template 52 | const createTemplate = (proxy: any) => 53 | async function (data: any) { 54 | return await proxy.$http.post(`/rule/template`, data); 55 | }; 56 | 57 | // 测试 template 58 | const testTemplate = (proxy: any) => 59 | async function (data: any) { 60 | return await proxy.$http.post(`/rule/test`, data); 61 | }; 62 | 63 | // 开启 template 64 | const switchTemplate = (proxy: any) => 65 | async function (data: any) { 66 | return await proxy.$http.post(`/rule/switch`, data); 67 | }; 68 | 69 | export default function createRuleApi(proxy: any) { 70 | return { 71 | getRules: getRules(proxy), 72 | getRuleNum: getRuleNum(proxy), 73 | getIgnore: getIgnore(proxy), 74 | updateIgnore: updateIgnore(proxy), 75 | getTemplateList: getTemplateList(proxy), 76 | getTemplateById: getTemplateById(proxy), 77 | deleteTemplateById: deleteTemplateById(proxy), 78 | updateTemplate: updateTemplate(proxy), 79 | createTemplate: createTemplate(proxy), 80 | testTemplate: testTemplate(proxy), 81 | switchTemplate: switchTemplate(proxy), 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Prizrak-Box", 3 | "productName": "Prizrak-Box", 4 | "version": "1.0.1", 5 | "description": "A Simple Mihomo GUI", 6 | "main": ".vite/build/main.js", 7 | "scripts": { 8 | "start": "electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "lint": "eslint --ext .ts,.tsx ." 13 | }, 14 | "keywords": [], 15 | "author": { 16 | "name": "legiz-ru", 17 | "email": "legiz-ru@gmail.com" 18 | }, 19 | "license": "GPL-3.0", 20 | "devDependencies": { 21 | "@electron-forge/cli": "^7.8.1", 22 | "@electron-forge/maker-deb": "^7.8.1", 23 | "@electron-forge/maker-dmg": "^7.8.1", 24 | "@electron-forge/maker-rpm": "^7.8.1", 25 | "@electron-forge/maker-wix": "^7.8.1", 26 | "@electron-forge/maker-zip": "^7.8.1", 27 | "@electron-forge/plugin-auto-unpack-natives": "^7.8.1", 28 | "@electron-forge/plugin-fuses": "^7.8.1", 29 | "@electron-forge/plugin-vite": "^7.8.1", 30 | "@electron-forge/shared-types": "^7.8.1", 31 | "@electron/fuses": "^1.8.0", 32 | "@eslint/js": "^9.25.1", 33 | "@iconify-json/ep": "^1.2.2", 34 | "@iconify-json/mdi": "^1.2.3", 35 | "@types/chroma-js": "^3.1.1", 36 | "@types/express": "^5.0.1", 37 | "@typescript-eslint/eslint-plugin": "^5.62.0", 38 | "@typescript-eslint/parser": "^5.62.0", 39 | "@vitejs/plugin-vue": "^5.2.4", 40 | "electron": "^36.2.0", 41 | "eslint": "^8.57.1", 42 | "eslint-plugin-import": "^2.31.0", 43 | "globals": "^16.0.0", 44 | "ts-node": "^10.9.2", 45 | "typescript": "~5.8.3", 46 | "typescript-eslint": "^8.31.0", 47 | "unplugin-auto-import": "19.1.2", 48 | "unplugin-icons": "22.1.0", 49 | "unplugin-vue-components": "28.5.0", 50 | "vite": "^6.3.5" 51 | }, 52 | "dependencies": { 53 | "@element-plus/icons-vue": "^2.3.1", 54 | "@intlify/unplugin-vue-i18n": "^6.0.8", 55 | "ace-builds": "^1.40.1", 56 | "apexcharts": "^4.7.0", 57 | "auto-launch": "^5.0.6", 58 | "axios": "^1.8.4", 59 | "chroma-js": "^3.1.2", 60 | "colorthief": "^2.6.0", 61 | "date-fns": "^4.1.0", 62 | "electron-log": "^5.4.0", 63 | "electron-store": "^10.0.1", 64 | "element-plus": "^2.9.8", 65 | "express": "^5.1.0", 66 | "lodash": "^4.17.21", 67 | "pinia": "3.0.2", 68 | "pinia-plugin-persistedstate": "^4.2.0", 69 | "spark-md5": "^3.0.2", 70 | "vue": "^3.5.13", 71 | "vue-i18n": "^11.1.3", 72 | "vue-router": "^4.5.0", 73 | "vue3-ace-editor": "^2.2.4", 74 | "vue3-apexcharts": "^1.8.0" 75 | }, 76 | "config": { 77 | "forge": "./forge.config.ts" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /doc/README.en.md: -------------------------------------------------------------------------------- 1 |
2 | Prizrak-Box 3 |

Prizrak-Box

4 |

A simple desktop client for Mihomo

5 |
6 | 7 | ## Download 8 | 9 | [Download the App](https://github.com/legiz-ru/Prizrak-Box/releases) 10 | 11 | ## Features 12 | 13 | - Supports local HTTP/HTTPS/SOCKS proxies 14 | - Supports Vmess, Vless, Shadowsocks, Trojan, Tuic, Hysteria, Hysteria2, Wireguard, and Mieru protocols 15 | - Supports parsing of share links, subscription links, Base64 format, and YAML format 16 | - Built-in subscription converter to convert various subscription types into Mihomo-compatible configurations 17 | - Automatically adds minimal rule groups to unruly subscriptions 18 | - DNS overwrite option to prevent DNS leaks 19 | - Unified rules and group settings for all subscriptions 20 | - Supports TUN mode 21 | 22 | ## Deeplink Import 23 | 24 | - Profiles can be imported directly by opening a link in the form `prizrak-box://install-config?url=https://sub.example.com/username` 25 | - Extra parameters in the deeplink are ignored, while query parameters inside the subscription URL are preserved 26 | 27 | ## Supported Platforms 28 | 29 | - Windows 10/11 (AMD64 / ARM64) 30 | - macOS 11.0+ (AMD64 / ARM64) 31 | - Linux (AMD64 / ARM64) 32 | 33 | ## How to Enable TUN Mode 34 | 35 | - Go to `Settings` → `Enable Authorization` → Restart the app → When the authorization prompt appears, grant 36 | permission → TUN mode can then be enabled in the app 37 | 38 | ## Note: Px Requires Network Access 39 | 40 | - When prompted, click "Allow" to grant network access 41 | 42 | ## Common macOS Issues 43 | 44 | - See [mac.md](mac/mac.md) 45 | 46 | ## Major Improvements in the Latest Version 47 | 48 | 1. Redesigned interface with support for theme switching, language switching, and drag-and-drop import 49 | 2. Search bar at the top to quickly switch between nodes in the current configuration 50 | 3. Added support for minimizing to system tray 51 | 4. Unified rule templates: 52 | - Simple groups for lightweight users 53 | - Multi-region groups 54 | - Full rule groups for advanced users 55 | 5. Web scraping and import/export modules from version 0.2 are not yet included 56 | 57 | ## Todo / Future Plans 58 | 59 | - Web scraping module 60 | - Import/export module 61 | - Bug fixes 62 | 63 | ## Preview 64 | 65 | | Tab | New Interface with Different Themes | 66 | |----------|-------------------------------------| 67 | | Home | ![General](img/home.jpg) | 68 | | Settings | ![Setting](img/setting.jpg) | 69 | | Proxies | ![Proxies](img/proxies.jpg) | 70 | | Profiles | ![Profiles](img/profiles.jpg) | 71 | -------------------------------------------------------------------------------- /src/components/menu/Off.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 67 | 68 | 110 | -------------------------------------------------------------------------------- /src-go/api/handlers/ws.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/gobwas/ws" 10 | "github.com/gobwas/ws/wsutil" 11 | "github.com/metacubex/mihomo/log" 12 | "github.com/legiz-ru/prizrak-box/api/models" 13 | "github.com/legiz-ru/prizrak-box/pkg/cache" 14 | "github.com/legiz-ru/prizrak-box/pkg/constant" 15 | ) 16 | 17 | // 保存排序后的 Profile 文件 18 | func saveProfileOrder(w http.ResponseWriter, r *http.Request) { 19 | // 必须是 websocket 请求 20 | if !(r.Header.Get("Upgrade") == "websocket") { 21 | ErrorResponse(w, r, fmt.Errorf("must be a websocket connection")) 22 | return 23 | } 24 | 25 | // 升级为 WebSocket 连接 26 | conn, _, _, err := ws.UpgradeHTTP(r, w) 27 | if err != nil { 28 | return 29 | } 30 | defer func(conn net.Conn) { 31 | err := conn.Close() 32 | if err != nil { 33 | 34 | } 35 | }(conn) 36 | 37 | // 处理 WebSocket 消息 38 | for { 39 | msg, op, err := wsutil.ReadClientData(conn) 40 | if err != nil { 41 | break 42 | } 43 | 44 | // 解析消息 45 | var profiles []models.Profile 46 | if err := json.Unmarshal(msg, &profiles); err != nil { 47 | log.Errorln("Decode error:%v", err) 48 | break 49 | } 50 | 51 | // 保存配置文件顺序 52 | if len(profiles) > 0 { 53 | _ = cache.Put(constant.ProfileOrder, profiles) 54 | } 55 | 56 | // 回显消息 57 | err = wsutil.WriteServerMessage(conn, op, []byte("success")) 58 | if err != nil { 59 | log.Errorln("Read error:%v", err) 60 | break 61 | } 62 | } 63 | } 64 | 65 | // 保存排序后的 WebTest 文件 66 | func saveWebTestOrder(w http.ResponseWriter, r *http.Request) { 67 | // 必须是 websocket 请求 68 | if !(r.Header.Get("Upgrade") == "websocket") { 69 | ErrorResponse(w, r, fmt.Errorf("must be a websocket connection")) 70 | return 71 | } 72 | 73 | // 升级为 WebSocket 连接 74 | conn, _, _, err := ws.UpgradeHTTP(r, w) 75 | if err != nil { 76 | return 77 | } 78 | defer func(conn net.Conn) { 79 | err := conn.Close() 80 | if err != nil { 81 | 82 | } 83 | }(conn) 84 | 85 | // 处理 WebSocket 消息 86 | for { 87 | msg, op, err := wsutil.ReadClientData(conn) 88 | if err != nil { 89 | break 90 | } 91 | 92 | // 解析消息 93 | var webTests []models.WebTest 94 | if err := json.Unmarshal(msg, &webTests); err != nil { 95 | log.Errorln("Decode error:%v", err) 96 | break 97 | } 98 | 99 | // 保存配置文件顺序 100 | if len(webTests) > 0 { 101 | _ = cache.Put(constant.WebTestOrder, webTests) 102 | } 103 | 104 | // 回显消息 105 | err = wsutil.WriteServerMessage(conn, op, []byte("success")) 106 | if err != nil { 107 | log.Errorln("Read error:%v", err) 108 | break 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/views/rule/Ignore.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 59 | 60 | -------------------------------------------------------------------------------- /src/util/pLoad.ts: -------------------------------------------------------------------------------- 1 | import {ElLoading, ElMessage} from "element-plus"; 2 | 3 | function translateErrorSegment(key: string, fallback: string): string { 4 | const translator = (window as any)?.pxTranslate; 5 | if (typeof translator === "function") { 6 | try { 7 | const result = translator(key); 8 | if (typeof result === "string" && result !== key) { 9 | return result; 10 | } 11 | } catch (error) { 12 | console.error("Failed to translate error segment", error); 13 | } 14 | } 15 | 16 | return fallback; 17 | } 18 | 19 | function normalizeErrorMessage(message: unknown): string { 20 | if (message === undefined || message === null) { 21 | return ""; 22 | } 23 | 24 | const text = typeof message === "string" ? message : String(message); 25 | 26 | const replacements: Array<[RegExp, string, string]> = [ 27 | [/请求失败/g, "errors.request-failed", "Request failed"], 28 | [/发送请求失败/g, "errors.request-send-failed", "Failed to send request"], 29 | [/创建请求失败/g, "errors.request-create-failed", "Failed to create request"], 30 | [/响应内容为空/g, "errors.response-empty", "Response body is empty"], 31 | [/未知原因/g, "errors.unknown-reason", "Unknown reason"], 32 | ]; 33 | 34 | let translated = text; 35 | for (const [pattern, key, fallback] of replacements) { 36 | if (pattern.test(translated)) { 37 | const value = translateErrorSegment(key, fallback); 38 | translated = translated.replace(pattern, value); 39 | } 40 | } 41 | 42 | return translated; 43 | } 44 | 45 | export async function pLoad(tip: any, callback: any) { 46 | const loading = ElLoading.service({ 47 | lock: true, 48 | text: tip, 49 | background: "rgba(0, 0, 0, 0.2)", 50 | }); 51 | await callback(); 52 | loading.close(); 53 | } 54 | 55 | 56 | export async function copy(textToCopy: any, t: any) { 57 | try { 58 | await navigator.clipboard.writeText(textToCopy); 59 | pSuccess(t("copy.success")); 60 | } catch (error) { 61 | error(t("copy.fail")); 62 | } 63 | } 64 | 65 | export function pSuccess(msg: any) { 66 | ElMessage({ 67 | message: msg, 68 | type: "success", 69 | grouping: true 70 | }); 71 | } 72 | 73 | export function pError(msg: any) { 74 | ElMessage({ 75 | message: normalizeErrorMessage(msg), 76 | type: "error", 77 | duration: 5000, 78 | grouping: true 79 | }); 80 | } 81 | 82 | export function pWarning(msg: any) { 83 | ElMessage({ 84 | message: msg, 85 | type: "warning", 86 | duration: 5000, 87 | grouping: true 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/api/home/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import SparkMD5 from "spark-md5"; 3 | import {WebTest} from "@/types/webtest"; 4 | 5 | // 获取版本 6 | const getVersion = (proxy: any) => 7 | async function () { 8 | const data = await proxy.$http.get("/version"); 9 | return data["version"]; 10 | }; 11 | 12 | // 获取 Mihomo 基本配置 13 | const getConfigs = (proxy: any) => 14 | async function () { 15 | return await proxy.$http.get("/configs"); 16 | }; 17 | 18 | // 更新 Mihomo 基本配置 19 | const updateConfigs = (proxy: any) => 20 | async function (configs: any) { 21 | return await proxy.$http.patch("/configs", configs); 22 | }; 23 | 24 | // 获取 分组 md5 25 | const getGroupMd5 = (proxy: any) => 26 | async function () { 27 | const data = await proxy.$http.get("/group"); 28 | const proxies = data["proxies"]; 29 | if (!proxies) { 30 | return "" 31 | } 32 | const end: any[] = []; 33 | for (const proxy of proxies) { 34 | end.push({ 35 | name: proxy["name"], 36 | now: proxy["now"], 37 | }); 38 | } 39 | // 排序 40 | end.sort((obj1, obj2) => obj1.name.localeCompare(obj2.name)); 41 | // 服务器md5 42 | const jsonString = JSON.stringify(end); 43 | return SparkMD5.hash(jsonString); 44 | }; 45 | 46 | // 获取 WebTest 列表 47 | const getWebTest = (proxy: any) => 48 | async function () { 49 | return await proxy.$http.get("/webtest"); 50 | }; 51 | 52 | // 删除 WebTest 53 | const deleteWebTest = (proxy: any) => 54 | async function (webTest: WebTest) { 55 | return await proxy.$http.post("/webtest/delete", webTest); 56 | }; 57 | 58 | // 修改 WebTest 59 | const updateWebTest = (proxy: any) => 60 | async function (webTest: WebTest) { 61 | return await proxy.$http.put("/webtest", webTest); 62 | }; 63 | 64 | // 获取 delay 65 | const getWebTestDelay = (proxy: any) => 66 | async function (list: WebTest[]): Promise { 67 | return await proxy.$http.post("/webtest/delay", list); 68 | }; 69 | 70 | // 获取落地 ip 71 | const getWebTestIp = (proxy: any) => 72 | async function (data: any) { 73 | return await proxy.$http.post("/webtest/ip", data); 74 | }; 75 | 76 | export default function createHomeApi(proxy: any) { 77 | return { 78 | getVersion: getVersion(proxy), 79 | getConfigs: getConfigs(proxy), 80 | getGroupMd5: getGroupMd5(proxy), 81 | updateConfigs: updateConfigs(proxy), 82 | getWebTest: getWebTest(proxy), 83 | deleteWebTest: deleteWebTest(proxy), 84 | updateWebTest: updateWebTest(proxy), 85 | getWebTestDelay: getWebTestDelay(proxy), 86 | getWebTestIp: getWebTestIp(proxy), 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /src/components/MyEvent.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/views/Rule.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 62 | 63 | -------------------------------------------------------------------------------- /src-go/pkg/utils/httpclient_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGenerateHWID(t *testing.T) { 8 | resetCachedDeviceDetailsForTest() 9 | 10 | hwid1 := generateHWID() 11 | hwid2 := generateHWID() 12 | 13 | if hwid1 == "" { 14 | t.Fatal("HWID should not be empty") 15 | } 16 | 17 | if hwid1 != hwid2 { 18 | t.Errorf("HWID should be consistent across calls, got '%s' and '%s'", hwid1, hwid2) 19 | } 20 | } 21 | 22 | func TestBuildDeviceHeaders(t *testing.T) { 23 | resetCachedDeviceDetailsForTest() 24 | 25 | // Test with HWID disabled 26 | config := &HTTPClientConfig{ 27 | EnableHWID: false, 28 | } 29 | UpdateHTTPClientConfig(config) 30 | 31 | headers := buildDeviceHeaders() 32 | if headers != nil { 33 | t.Errorf("Expected nil headers when HWID is disabled, got %v", headers) 34 | } 35 | 36 | // Test with HWID enabled 37 | config = &HTTPClientConfig{ 38 | EnableHWID: true, 39 | DeviceOS: "Linux", 40 | DeviceOSVer: "5.4.0", 41 | DeviceModel: "TestDevice", 42 | } 43 | UpdateHTTPClientConfig(config) 44 | 45 | headers = buildDeviceHeaders() 46 | if headers == nil { 47 | t.Fatal("Expected headers when HWID is enabled, got nil") 48 | } 49 | 50 | if _, exists := headers["x-hwid"]; !exists { 51 | t.Error("Expected x-hwid header") 52 | } 53 | 54 | if headers["x-device-os"] != "Linux" { 55 | t.Errorf("Expected x-device-os to be 'Linux', got '%s'", headers["x-device-os"]) 56 | } 57 | 58 | if headers["x-ver-os"] != "5.4.0" { 59 | t.Errorf("Expected x-ver-os to be '5.4.0', got '%s'", headers["x-ver-os"]) 60 | } 61 | 62 | if headers["x-device-model"] != "TestDevice" { 63 | t.Errorf("Expected x-device-model to be 'TestDevice', got '%s'", headers["x-device-model"]) 64 | } 65 | 66 | config = &HTTPClientConfig{ 67 | EnableHWID: true, 68 | DeviceOS: "Windows x64", 69 | } 70 | UpdateHTTPClientConfig(config) 71 | 72 | headers = buildDeviceHeaders() 73 | if headers["x-device-os"] != "Windows" { 74 | t.Errorf("Expected x-device-os to be 'Windows', got '%s'", headers["x-device-os"]) 75 | } 76 | } 77 | 78 | func TestUpdateHTTPClientConfig(t *testing.T) { 79 | // Test user agent with HWID enabled 80 | config := &HTTPClientConfig{ 81 | EnableHWID: true, 82 | Version: "1.0.1", 83 | } 84 | UpdateHTTPClientConfig(config) 85 | 86 | expected := "prizrak-box/1.0.1" 87 | if globalConfig.UserAgent != expected { 88 | t.Errorf("Expected user agent '%s', got '%s'", expected, globalConfig.UserAgent) 89 | } 90 | 91 | // Test user agent with HWID disabled 92 | config = &HTTPClientConfig{ 93 | EnableHWID: false, 94 | } 95 | UpdateHTTPClientConfig(config) 96 | 97 | expected = "clash-verge/v2.3.0" 98 | if globalConfig.UserAgent != expected { 99 | t.Errorf("Expected user agent '%s', got '%s'", expected, globalConfig.UserAgent) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src-go/pkg/utils/device_details.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | type DeviceDetails struct { 10 | HWID string `json:"hwid"` 11 | OS string `json:"os"` 12 | OSVersion string `json:"osVersion"` 13 | Model string `json:"model"` 14 | } 15 | 16 | var ( 17 | deviceDetailsOnce sync.Once 18 | cachedDeviceDetails DeviceDetails 19 | ) 20 | 21 | func GetDeviceDetails() DeviceDetails { 22 | deviceDetailsOnce.Do(func() { 23 | cachedDeviceDetails = ensureDeviceDetails(collectDeviceDetails()) 24 | }) 25 | return cachedDeviceDetails 26 | } 27 | 28 | func generateHWID() string { 29 | return GetDeviceDetails().HWID 30 | } 31 | 32 | func ensureDeviceDetails(details DeviceDetails) DeviceDetails { 33 | details.HWID = strings.TrimSpace(details.HWID) 34 | details.OS = normalizeOSName(details.OS) 35 | details.OSVersion = strings.TrimSpace(details.OSVersion) 36 | details.Model = strings.TrimSpace(details.Model) 37 | 38 | if details.HWID == "" { 39 | details.HWID = generateRawHWID() 40 | } 41 | 42 | if details.OS == "" { 43 | details.OS = defaultOSName() 44 | } 45 | 46 | if details.Model == "" { 47 | details.Model = runtime.GOARCH 48 | } 49 | 50 | return details 51 | } 52 | 53 | func defaultOSName() string { 54 | switch runtime.GOOS { 55 | case "windows": 56 | return "Windows" 57 | case "darwin": 58 | return "macOS" 59 | case "linux": 60 | return "Linux" 61 | default: 62 | return runtime.GOOS 63 | } 64 | } 65 | 66 | func normalizeOSName(value string) string { 67 | trimmed := strings.TrimSpace(value) 68 | lower := strings.ToLower(trimmed) 69 | 70 | switch { 71 | case lower == "": 72 | return defaultOSName() 73 | case strings.Contains(lower, "windows"): 74 | return "Windows" 75 | case strings.Contains(lower, "macos"), strings.Contains(lower, "mac os"), strings.Contains(lower, "darwin"): 76 | return "macOS" 77 | case strings.Contains(lower, "linux"), 78 | strings.Contains(lower, "ubuntu"), 79 | strings.Contains(lower, "debian"), 80 | strings.Contains(lower, "fedora"), 81 | strings.Contains(lower, "centos"), 82 | strings.Contains(lower, "red hat"), 83 | strings.Contains(lower, "rhel"), 84 | strings.Contains(lower, "suse"), 85 | strings.Contains(lower, "opensuse"), 86 | strings.Contains(lower, "arch"), 87 | strings.Contains(lower, "manjaro"), 88 | strings.Contains(lower, "mint"), 89 | strings.Contains(lower, "elementary"), 90 | strings.Contains(lower, "gentoo"), 91 | strings.Contains(lower, "alpine"), 92 | strings.Contains(lower, "void linux"), 93 | strings.Contains(lower, "endeavour"), 94 | strings.Contains(lower, "pop!_os"), 95 | strings.Contains(lower, "pop os"), 96 | strings.Contains(lower, "zorin"): 97 | return "Linux" 98 | default: 99 | return trimmed 100 | } 101 | } 102 | 103 | func resetCachedDeviceDetailsForTest() { 104 | deviceDetailsOnce = sync.Once{} 105 | cachedDeviceDetails = DeviceDetails{} 106 | } 107 | -------------------------------------------------------------------------------- /src/views/Log.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 65 | 66 | 129 | -------------------------------------------------------------------------------- /src/components/MyEditor.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 68 | 69 | 135 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import createProxiesApi from "./proxies"; 2 | import createHomeApi from "@/api/home"; 3 | import createConnApi from "@/api/connections"; 4 | import createRuleApi from "./rule"; 5 | import createProfilesApi from "@/api/profiles"; 6 | import createDnsApi from "@/api/dns"; 7 | import createMihomoApi from "@/api/mihomo"; 8 | import createPrizrakApi from "@/api/prizrak"; 9 | 10 | export default function createApi(proxy: any) { 11 | return { 12 | getDelay: createProxiesApi(proxy).getDelay, 13 | getGroups: createProxiesApi(proxy).getGroups, 14 | getProxies: createProxiesApi(proxy).getProxies, 15 | setProxy: createProxiesApi(proxy).setProxy, 16 | getVersion: createHomeApi(proxy).getVersion, 17 | getConfigs: createHomeApi(proxy).getConfigs, 18 | getGroupMd5: createHomeApi(proxy).getGroupMd5, 19 | updateConfigs: createHomeApi(proxy).updateConfigs, 20 | getWebTest: createHomeApi(proxy).getWebTest, 21 | deleteWebTest: createHomeApi(proxy).deleteWebTest, 22 | updateWebTest: createHomeApi(proxy).updateWebTest, 23 | getWebTestDelay: createHomeApi(proxy).getWebTestDelay, 24 | getWebTestIp: createHomeApi(proxy).getWebTestIp, 25 | closeConnection: createConnApi(proxy).closeConnection, 26 | closeAllConnection: createConnApi(proxy).closeAllConnection, 27 | getRules: createRuleApi(proxy).getRules, 28 | getRuleNum: createRuleApi(proxy).getRuleNum, 29 | getIgnore: createRuleApi(proxy).getIgnore, 30 | updateIgnore: createRuleApi(proxy).updateIgnore, 31 | getTemplateList: createRuleApi(proxy).getTemplateList, 32 | getTemplateById: createRuleApi(proxy).getTemplateById, 33 | deleteTemplateById: createRuleApi(proxy).deleteTemplateById, 34 | updateTemplate: createRuleApi(proxy).updateTemplate, 35 | createTemplate: createRuleApi(proxy).createTemplate, 36 | testTemplate: createRuleApi(proxy).testTemplate, 37 | switchTemplate: createRuleApi(proxy).switchTemplate, 38 | addProfileFromInput: createProfilesApi(proxy).addProfileFromInput, 39 | addProfileFromFile: createProfilesApi(proxy).addProfileFromFile, 40 | deleteProfile: createProfilesApi(proxy).deleteProfile, 41 | updateProfile: createProfilesApi(proxy).updateProfile, 42 | getProfileList: createProfilesApi(proxy).getProfileList, 43 | refreshProfile: createProfilesApi(proxy).refreshProfile, 44 | switchProfile: createProfilesApi(proxy).switchProfile, 45 | getDNS: createDnsApi(proxy).getDNS, 46 | updateDNS: createDnsApi(proxy).updateDNS, 47 | switchDNS: createDnsApi(proxy).switchDNS, 48 | getMihomo: createMihomoApi(proxy).getMihomo, 49 | updateMihomo: createMihomoApi(proxy).updateMihomo, 50 | waitRunning: createMihomoApi(proxy).waitRunning, 51 | getAdmin: createMihomoApi(proxy).getAdmin, 52 | enableProxy: createPrizrakApi(proxy).enableProxy, 53 | disableProxy: createPrizrakApi(proxy).disableProxy, 54 | checkAddressPort: createPrizrakApi(proxy).checkAddressPort, 55 | configDir: createPrizrakApi(proxy).configDir, 56 | exit: createPrizrakApi(proxy).exit, 57 | updateHTTPClientConfig: createPrizrakApi(proxy).updateHTTPClientConfig, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src-go/api/job/refresh.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/metacubex/mihomo/log" 5 | "github.com/legiz-ru/prizrak-box/api/models" 6 | "github.com/legiz-ru/prizrak-box/internal" 7 | "github.com/legiz-ru/prizrak-box/pkg/cache" 8 | "github.com/legiz-ru/prizrak-box/pkg/constant" 9 | "github.com/legiz-ru/prizrak-box/pkg/cron" 10 | "github.com/legiz-ru/prizrak-box/pkg/proxy" 11 | "github.com/legiz-ru/prizrak-box/pkg/utils" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | var refreshLock sync.Mutex 19 | 20 | func RefreshJob() { 21 | cron.AddTask("Refresh", 30*time.Minute, DoRefresh) 22 | } 23 | 24 | func DoRefresh() { 25 | if refreshLock.TryLock() { 26 | defer refreshLock.Unlock() 27 | } else { 28 | return 29 | } 30 | 31 | log.Infoln("[Refresh] job start") 32 | 33 | // 获取需要更新的订阅 34 | var profiles []models.Profile 35 | _ = cache.GetList(constant.PrefixProfile, &profiles) 36 | if profiles == nil || len(profiles) == 0 { 37 | return 38 | } 39 | 40 | // 过滤远程订阅 41 | var filteredProfiles []models.Profile 42 | for _, profile := range profiles { 43 | if profile.Type != 1 || profile.Interval == "" { 44 | continue 45 | } 46 | 47 | // 获取更新间隔 48 | interval, err := strconv.Atoi(profile.Interval) 49 | if err != nil { 50 | continue 51 | } 52 | 53 | // 计算上次更新时间到现在时间的间隔 54 | diff := utils.GetHourDiff(profile.GetUpdateTime()) 55 | if diff < interval { 56 | continue 57 | } 58 | 59 | filteredProfiles = append(filteredProfiles, profile) 60 | } 61 | 62 | log.Infoln("[Refresh] job find %d profile need fresh", len(filteredProfiles)) 63 | 64 | // 进行更新逻辑 65 | for _, fp := range filteredProfiles { 66 | // 获取指针 67 | profile := &fp 68 | 69 | log.Infoln("[Refresh] job profile %v fresh start", profile.Title) 70 | 71 | // 进行更新 72 | title := profile.Title 73 | 74 | // 发送请求 75 | sub := profile.Content 76 | headers := map[string]string{} 77 | res, err := utils.FastGet(sub, headers, proxy.GetProxyUrl()) 78 | if err != nil { 79 | log.Errorln("[Refresh] Sub=%s, URL = %s, Request Error:%v", title, sub, err) 80 | continue 81 | } 82 | 83 | // 解析存盘 84 | err = internal.Resolve(res.Body, profile, true) 85 | if err == nil { 86 | // 进行请求头解析 87 | mergedHeaders := internal.MergeHeaders(res.Headers, internal.ParseInlineHeaders(res.Body)) 88 | internal.ParseHeaders(mergedHeaders, sub, profile) 89 | if title != "" { 90 | profile.Title = title 91 | } 92 | UpdateDb(profile, 1) 93 | 94 | log.Infoln("[Refresh] job profile %v fresh success", profile.Title) 95 | } else { 96 | log.Errorln("[Refresh] Sub=%s, URL = %s, Resolve Error:%v", title, sub, err) 97 | } 98 | } 99 | } 100 | 101 | // UpdateDb 更新数据库 102 | func UpdateDb(profile *models.Profile, kind int) { 103 | profile.Type = kind 104 | profile.SetUpdateTime() 105 | if kind == 2 { 106 | profile.Content = "" 107 | } 108 | checkTitle(profile) 109 | _ = cache.Put(profile.Id, *profile) 110 | } 111 | 112 | func checkTitle(profile *models.Profile) { 113 | profile.Title = strings.TrimSpace(profile.Title) 114 | if profile.Title != "" { 115 | return 116 | } 117 | if profile.Type == 1 { 118 | profile.Title = "Sub-" + utils.GetDateTime() 119 | } else if profile.Type == 2 { 120 | profile.Title = "Local-" + utils.GetDateTime() 121 | } else { 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Prizrak-Box 4 | 5 |

Prizrak-Box

6 | 7 |

🌈 A simple desktop client for Mihomo

8 |

✨ 一个简易的 Mihomo 桌面客户端

9 |

✨ Простой настольный клиент для Mihomo

10 | 11 |

12 | 🇨🇳 简体中文 | 🇺🇸 English | 🇷🇺 Русский 13 |

14 | 15 |
16 | 17 | --- 18 | 19 | ## 📦 Project Overview | 项目简介 | Обзор проекта 20 | 21 | **Prizrak-Box** is a lightweight and user-friendly cross-platform client (fork of [Pandora-Box](https://github.com/snakem982/Pandora-Box)) for [Mihomo](https://github.com/MetaCubeX/mihomo), supporting multiple proxy protocols, automatic rule grouping, TUN mode, and functionality relevant for Russia. 22 | It is designed for both casual and advanced users to easily manage and convert proxy subscriptions. 23 | 24 | **Prizrak-Box** 是一个跨平台的轻量桌面客户端([Pandora-Box](https://github.com/snakem982/Pandora-Box) 的分支项目),适配 [Mihomo](https://github.com/MetaCubeX/mihomo) 内核,支持多种代理协议、规则自动分组与 TUN 模式,并具备适用于俄罗斯的相关功能。界面简洁,功能强大,适合轻量与进阶用户使用。 25 | 26 | **Prizrak-Box** — это легкий и удобный кроссплатформенный клиент (форк [Pandora-Box](https://github.com/snakem982/Pandora-Box)) для [Mihomo](https://github.com/MetaCubeX/mihomo), поддерживающий различные прокси-протоколы, автоматическую группировку правил, режим TUN и функционал, актуальный для России. 27 | Он разработан как для обычных пользователей, так и для продвинутых, чтобы облегчить управление и конвертацию подписок прокси. 28 | 29 | --- 30 | 31 | ## 📥 Get Started | 快速开始 | Начало работы 32 | 33 | 👉 [Download the Latest Release / 下载最新版本 / Скачать последнюю версию](https://github.com/legiz-ru/Prizrak-Box/releases) 34 | 35 | --- 36 | 37 | ## 🛠 Development | 开发 | Разработка 38 | 39 | If you want to contribute or build Prizrak-Box locally, refer to the resources below: 40 | 如果你想参与开发或构建 Prizrak-Box,可以参考以下资源: 41 | Если вы хотите принять участие в разработке или собрать Prizrak-Box локально, воспользуйтесь следующими ресурсами: 42 | 43 | ### 🔧 Prerequisites | 前置依赖 | Предварительные требования 44 | 45 | - [Node.js](https://nodejs.org/) ≥ 18 (for building UI components or tooling) 46 | - [Go](https://go.dev/) ≥ 1.24 (for integration with Mihomo or backend modules) 47 | 48 | ### 🧪 Build Instructions | 构建指南 | Инструкции по сборке 49 | 50 | ```bash 51 | # Install dependencies 52 | npm install 53 | cd src-go 54 | go mod tidy 55 | 56 | # Build px backend 57 | CGO_ENABLED=0 go build -tags=with_gvisor -trimpath -ldflags "-X github.com/legiz-ru/prizrak-box/api.Version=v-test" -o px(.exe) 58 | cd .. 59 | 60 | # Build desktop app 61 | npm run package 62 | 63 | # Run in dev mode 64 | npm run start 65 | ``` 66 | 67 | --- 68 | 69 | ## 🌐 Language | 语言选择 | Выбор языка 70 | 71 | - 🇨🇳 [查看中文文档](doc/README.zh-CN.md) 72 | - 🇺🇸 [View English Documentation](doc/README.en.md) 73 | - 🇷🇺 [Просмотр русской документации](doc/README.ru.md) 74 | 75 | --- 76 | 77 | ## 🧭 More Information | 更多信息 | Дополнительная информация 78 | 79 | - ✅ [Project Issues](https://github.com/legiz-ru/Prizrak-Box/issues) 80 | - 📄 [License (GPL-3.0)](./LICENSE) 81 | 82 | --- 83 | 84 | 📝 This README was generated with the assistance of AI and reviewed by the developer. 85 | 📝 本文档内容由 AI 辅助生成,并由开发者校对。 86 | 📝 Этот README создан при поддержке ИИ и проверен разработчиком. 87 | -------------------------------------------------------------------------------- /src/components/MyDrop.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 93 | 94 | 120 | -------------------------------------------------------------------------------- /src/components/dnd/VDContainer/src/VDContainer.vue: -------------------------------------------------------------------------------- 1 | 23 | 81 | 82 | 121 | -------------------------------------------------------------------------------- /src/components/menu/Language.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 96 | 97 | 141 | -------------------------------------------------------------------------------- /src/components/DeepLinkImportOverlay.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 51 | 52 | 119 | -------------------------------------------------------------------------------- /src-go/pkg/utils/device_details_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package utils 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/denisbrodbeck/machineid" 10 | "github.com/yusufpapurcu/wmi" 11 | "golang.org/x/sys/windows/registry" 12 | ) 13 | 14 | type win32ComputerSystem struct { 15 | Model string 16 | } 17 | 18 | type win32BaseBoard struct { 19 | Product string 20 | } 21 | 22 | func collectDeviceDetails() DeviceDetails { 23 | details := DeviceDetails{} 24 | 25 | if guid, err := readRegistryString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, "MachineGuid"); err == nil { 26 | details.HWID = guid 27 | } 28 | 29 | if details.HWID == "" { 30 | if id, err := machineid.ProtectedID("prizrak-box"); err == nil { 31 | details.HWID = id 32 | } 33 | } 34 | 35 | if productName, err := readRegistryString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "ProductName"); err == nil { 36 | details.OS = productName 37 | } 38 | 39 | major, _ := readRegistryUint32(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "CurrentMajorVersionNumber") 40 | minor, _ := readRegistryUint32(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "CurrentMinorVersionNumber") 41 | build, _ := readRegistryString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "CurrentBuildNumber") 42 | if build == "" { 43 | build, _ = readRegistryString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "CurrentBuild") 44 | } 45 | 46 | if major != 0 || minor != 0 || build != "" { 47 | if build == "" { 48 | build = "0" 49 | } 50 | details.OSVersion = fmt.Sprintf("%d.%d.%s", major, minor, build) 51 | } 52 | 53 | if details.OSVersion == "" { 54 | if version, err := readRegistryString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "ReleaseId"); err == nil && version != "" { 55 | details.OSVersion = version 56 | } 57 | } 58 | 59 | // Hardware model 60 | var systems []win32ComputerSystem 61 | if err := wmi.Query("SELECT Model FROM Win32_ComputerSystem", &systems); err == nil && len(systems) > 0 { 62 | details.Model = strings.TrimSpace(systems[0].Model) 63 | } 64 | 65 | var boards []win32BaseBoard 66 | if err := wmi.Query("SELECT Product FROM Win32_BaseBoard", &boards); err == nil && len(boards) > 0 { 67 | product := strings.TrimSpace(boards[0].Product) 68 | if product != "" { 69 | if details.Model == "" { 70 | details.Model = product 71 | } else if !strings.EqualFold(details.Model, product) { 72 | details.Model = details.Model + "/" + product 73 | } 74 | } 75 | } 76 | 77 | if details.Model == "" { 78 | if pretty, err := readRegistryString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "ProductName"); err == nil { 79 | details.Model = pretty 80 | } 81 | } 82 | 83 | return details 84 | } 85 | 86 | func readRegistryString(root registry.Key, path, name string) (string, error) { 87 | key, err := registry.OpenKey(root, path, registry.QUERY_VALUE) 88 | if err != nil { 89 | return "", err 90 | } 91 | defer key.Close() 92 | 93 | value, _, err := key.GetStringValue(name) 94 | if err != nil { 95 | return "", err 96 | } 97 | return strings.TrimSpace(value), nil 98 | } 99 | 100 | func readRegistryUint32(root registry.Key, path, name string) (uint32, error) { 101 | key, err := registry.OpenKey(root, path, registry.QUERY_VALUE) 102 | if err != nil { 103 | return 0, err 104 | } 105 | defer key.Close() 106 | 107 | value, _, err := key.GetIntegerValue(name) 108 | if err != nil { 109 | return 0, err 110 | } 111 | return uint32(value), nil 112 | } 113 | -------------------------------------------------------------------------------- /src-electron/launch.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import {lookup} from 'dns/promises'; 3 | import {app} from 'electron'; 4 | // @ts-ignore 5 | import AutoLaunch from 'auto-launch'; 6 | import log from './log'; 7 | import {storeGet, storeSet} from './store'; 8 | 9 | const APP_NAME = 'Prizrak-Box'; 10 | const BOOT_FLAG = '--boot-launch'; 11 | 12 | let autoLauncher = createAutoLauncher(); 13 | 14 | /** 15 | * 创建 AutoLaunch 实例 16 | */ 17 | function createAutoLauncher(): AutoLaunch { 18 | return new AutoLaunch({ 19 | name: APP_NAME, 20 | path: app.getPath('exe'), 21 | args: [BOOT_FLAG], 22 | }); 23 | } 24 | 25 | /** 26 | * 等待网络就绪(能解析指定域名),超时返回 false 27 | * @param timeout 超时时间(默认 30 秒) 28 | * @param host 检测的主机(默认 bing.com) 29 | */ 30 | export async function waitForNetworkReady(timeout = 30000, host = 'bing.com'): Promise { 31 | const deadline = Date.now() + timeout; 32 | while (Date.now() < deadline) { 33 | try { 34 | await lookup(host); 35 | return true; 36 | } catch { 37 | await new Promise(res => setTimeout(res, 1000)); 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | /** 44 | * 判断当前是否由开机自启启动 45 | */ 46 | export async function isBootAutoLaunch(): Promise { 47 | const uptime = os.uptime(); 48 | const launchedByFlag = process.argv.includes(BOOT_FLAG); 49 | const launchedSoonAfterBoot = uptime < 30; 50 | 51 | let wasOpenedAtLogin = false; 52 | try { 53 | wasOpenedAtLogin = app.getLoginItemSettings?.().wasOpenedAtLogin ?? false; 54 | } catch { 55 | // 忽略不支持的平台 56 | } 57 | 58 | log.info('process.argv is', process.argv); 59 | return launchedByFlag || wasOpenedAtLogin || launchedSoonAfterBoot; 60 | } 61 | 62 | /** 63 | * 启用开机自启 64 | */ 65 | export async function enableAutoLaunch(): Promise { 66 | try { 67 | if (!(await autoLauncher.isEnabled())) { 68 | await autoLauncher.enable(); 69 | storeSet('autoLaunch.lastRegisteredExe', app.getPath('exe')); 70 | log.info('✅ 开启开机自启'); 71 | } else { 72 | log.info('开机自启已启用'); 73 | } 74 | } catch (err) { 75 | log.error('开启开机自启失败:', err); 76 | } 77 | } 78 | 79 | /** 80 | * 禁用开机自启 81 | */ 82 | export async function disableAutoLaunch(): Promise { 83 | try { 84 | if (await autoLauncher.isEnabled()) { 85 | await autoLauncher.disable(); 86 | log.info('🛑 关闭开机自启'); 87 | } 88 | } catch (err) { 89 | log.error('关闭开机自启失败:', err); 90 | } 91 | } 92 | 93 | /** 94 | * 查询开机自启状态 95 | */ 96 | export async function isAutoLaunchEnabled(): Promise { 97 | try { 98 | return await autoLauncher.isEnabled(); 99 | } catch (err) { 100 | log.error('查询开机自启状态失败:', err); 101 | return false; 102 | } 103 | } 104 | 105 | /** 106 | * 更新开机自启注册项路径(如当前 exe 路径发生变化) 107 | */ 108 | export async function updateAutoLaunchRegistration(): Promise { 109 | try { 110 | const currentExe = app.getPath('exe'); 111 | const lastRegistered = storeGet('autoLaunch.lastRegisteredExe') as string | undefined; 112 | 113 | if ((await autoLauncher.isEnabled()) && currentExe !== lastRegistered) { 114 | await autoLauncher.disable(); 115 | 116 | autoLauncher = createAutoLauncher(); 117 | await autoLauncher.enable(); 118 | 119 | storeSet('autoLaunch.lastRegisteredExe', currentExe); 120 | log.info(`🆕 已更新开机自启路径: ${currentExe}`); 121 | } else { 122 | log.info('开机自启注册项无需更新'); 123 | } 124 | } catch (err) { 125 | log.error('更新开机自启注册项失败:', err); 126 | } 127 | } -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const computed: typeof import('vue')['computed'] 11 | const createApp: typeof import('vue')['createApp'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const effectScope: typeof import('vue')['effectScope'] 16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 17 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 18 | const h: typeof import('vue')['h'] 19 | const inject: typeof import('vue')['inject'] 20 | const isProxy: typeof import('vue')['isProxy'] 21 | const isReactive: typeof import('vue')['isReactive'] 22 | const isReadonly: typeof import('vue')['isReadonly'] 23 | const isRef: typeof import('vue')['isRef'] 24 | const markRaw: typeof import('vue')['markRaw'] 25 | const nextTick: typeof import('vue')['nextTick'] 26 | const onActivated: typeof import('vue')['onActivated'] 27 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 28 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 29 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 30 | const onDeactivated: typeof import('vue')['onDeactivated'] 31 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 32 | const onMounted: typeof import('vue')['onMounted'] 33 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 34 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 35 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 36 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 37 | const onUnmounted: typeof import('vue')['onUnmounted'] 38 | const onUpdated: typeof import('vue')['onUpdated'] 39 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 40 | const provide: typeof import('vue')['provide'] 41 | const reactive: typeof import('vue')['reactive'] 42 | const readonly: typeof import('vue')['readonly'] 43 | const ref: typeof import('vue')['ref'] 44 | const resolveComponent: typeof import('vue')['resolveComponent'] 45 | const shallowReactive: typeof import('vue')['shallowReactive'] 46 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 47 | const shallowRef: typeof import('vue')['shallowRef'] 48 | const toRaw: typeof import('vue')['toRaw'] 49 | const toRef: typeof import('vue')['toRef'] 50 | const toRefs: typeof import('vue')['toRefs'] 51 | const toValue: typeof import('vue')['toValue'] 52 | const triggerRef: typeof import('vue')['triggerRef'] 53 | const unref: typeof import('vue')['unref'] 54 | const useAttrs: typeof import('vue')['useAttrs'] 55 | const useCssModule: typeof import('vue')['useCssModule'] 56 | const useCssVars: typeof import('vue')['useCssVars'] 57 | const useId: typeof import('vue')['useId'] 58 | const useModel: typeof import('vue')['useModel'] 59 | const useSlots: typeof import('vue')['useSlots'] 60 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 61 | const watch: typeof import('vue')['watch'] 62 | const watchEffect: typeof import('vue')['watchEffect'] 63 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 64 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 65 | } 66 | // for type re-export 67 | declare global { 68 | // @ts-ignore 69 | export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 70 | import('vue') 71 | } 72 | -------------------------------------------------------------------------------- /src/components/menu/MyNav.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 103 | 104 | -------------------------------------------------------------------------------- /src/components/setting/MyPort.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 131 | 132 | 173 | -------------------------------------------------------------------------------- /src/components/setting/MyBind.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 133 | 134 | 175 | -------------------------------------------------------------------------------- /src-go/pkg/sys/proxy/proxy_windows.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "net/textproto" 8 | "strconv" 9 | "strings" 10 | 11 | sys "github.com/legiz-ru/prizrak-box/pkg/sys/cmd" 12 | ) 13 | 14 | func OffAll() error { 15 | if err := OffHttps(); err != nil { 16 | return err 17 | } 18 | if err := OffHttp(); err != nil { 19 | return err 20 | } 21 | if err := OffSocks(); err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | func SetIgnore(ignores []string) error { 28 | return set("ProxyOverride", "REG_SZ", strings.Join(ignores, ";")) 29 | } 30 | 31 | func ClearIgnore() error { 32 | return set("ProxyOverride", "REG_SZ", "") 33 | } 34 | 35 | func GetIgnore() ([]string, error) { 36 | m, err := get("ProxyOverride") 37 | if err != nil { 38 | return nil, err 39 | } 40 | ignores := m["ProxyOverride"] 41 | if ignores == "" { 42 | return []string{}, nil 43 | } 44 | return strings.Split(ignores, ";"), nil 45 | } 46 | 47 | func OnHttps(addr Addr) error { 48 | err := set("ProxyServer", "REG_SZ", addr.String()) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return useProxy(true) 54 | } 55 | 56 | func OffHttps() error { 57 | err := useProxy(false) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return set("ProxyServer", "REG_SZ", "") 63 | } 64 | 65 | func OnHttp(addr Addr) error { 66 | return nil 67 | } 68 | 69 | func OffHttp() error { 70 | return nil 71 | } 72 | 73 | func OnSocks(addr Addr) error { 74 | return nil 75 | } 76 | 77 | func OffSocks() error { 78 | return nil 79 | } 80 | 81 | func GetHttp() (*Addr, error) { 82 | // 检查代理是否启用 83 | enabled, err := getProxy() 84 | if err != nil { 85 | return nil, err 86 | } 87 | if !enabled { 88 | // 如果代理未启用,返回 nil 89 | return nil, nil 90 | } 91 | 92 | // 获取代理服务器地址 93 | m, err := get("ProxyServer") 94 | if err != nil { 95 | return nil, err 96 | } 97 | addr := m["ProxyServer"] 98 | if addr == "" { 99 | return nil, nil 100 | } 101 | 102 | // 解析 HTTP 代理地址 103 | if strings.Contains(addr, "=") { 104 | // 如果 ProxyServer 包含多个协议的代理地址,提取 http= 部分 105 | parts := strings.Split(addr, ";") 106 | for _, part := range parts { 107 | if strings.HasPrefix(part, "http=") { 108 | addr = strings.TrimPrefix(part, "http=") 109 | break 110 | } 111 | } 112 | } else { 113 | // 如果只有一个代理地址,直接使用 114 | addr = strings.TrimSpace(addr) 115 | } 116 | 117 | // 返回解析后的地址 118 | return ParseAddrPtr(addr), nil 119 | } 120 | 121 | const settingPath = `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings` 122 | 123 | func set(key string, typ string, value string) error { 124 | _, err := sys.Command(`reg`, `add`, settingPath, `/v`, key, `/t`, typ, `/d`, value, `/f`) 125 | return err 126 | } 127 | 128 | func get(keys ...string) (map[string]string, error) { 129 | buf, err := sys.Command(`reg`, `query`, settingPath) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return getFrom(buf, settingPath, keys...) 134 | } 135 | 136 | func del(key string) error { 137 | _, err := sys.Command(`reg`, `delete`, settingPath, `/v`, key, `/f`) 138 | return err 139 | } 140 | 141 | func strBool(b bool) string { 142 | if b { 143 | return "1" 144 | } 145 | return "0" 146 | } 147 | 148 | func useProxy(b bool) error { 149 | return set("ProxyEnable", "REG_DWORD", strBool(b)) 150 | } 151 | 152 | func getProxy() (bool, error) { 153 | m, err := get("ProxyEnable", "REG_DWORD") 154 | if err != nil { 155 | return false, err 156 | } 157 | i, err := strconv.ParseInt(m["ProxyEnable"], 0, 0) 158 | if err != nil { 159 | return false, err 160 | } 161 | return i != 0, nil 162 | } 163 | 164 | func getFrom(data string, path string, keys ...string) (map[string]string, error) { 165 | m := map[string]string{} 166 | index := strings.Index(data, path) 167 | if index == -1 { 168 | return m, nil 169 | } 170 | data = data[index+len(path):] 171 | reader := textproto.NewReader(bufio.NewReader(bytes.NewBufferString(data))) 172 | _, _ = reader.ReadLine() 173 | for len(m) != len(keys) { 174 | row, err := reader.ReadLine() 175 | if err != nil { 176 | if err == io.EOF { 177 | break 178 | } 179 | return nil, err 180 | } 181 | if row == "" { 182 | break 183 | } 184 | row = strings.TrimSpace(row) 185 | s := strings.SplitN(row, " ", 3) 186 | key := s[0] 187 | skip := true 188 | for _, k := range keys { 189 | if k == key { 190 | skip = false 191 | break 192 | } 193 | } 194 | if skip { 195 | continue 196 | } 197 | val := "" 198 | if len(s) == 3 { 199 | val = s[2] 200 | } 201 | m[key] = val 202 | } 203 | return m, nil 204 | } 205 | --------------------------------------------------------------------------------