Repository: GUI-for-Cores/GUI.for.SingBox Branch: main Commit: d3e2cfde49dd Files: 219 Total size: 847.6 KB Directory structure: gitextract_dgpyt8y7/ ├── .github/ │ └── workflows/ │ ├── release.yml │ └── rolling-release.yml ├── .gitignore ├── GUI.for.SingBox.code-workspace ├── LICENSE ├── README.md ├── bridge/ │ ├── bridge.go │ ├── exec.go │ ├── exec_others.go │ ├── exec_windows.go │ ├── io.go │ ├── mmdb.go │ ├── net.go │ ├── notification.go │ ├── server.go │ ├── tray.go │ ├── types.go │ └── utils.go ├── build/ │ ├── README.md │ ├── darwin/ │ │ ├── Info.dev.plist │ │ └── Info.plist │ └── windows/ │ ├── info.json │ └── wails.exe.manifest ├── frontend/ │ ├── .editorconfig │ ├── .gitattributes │ ├── .gitignore │ ├── .oxfmtrc.json │ ├── .oxlintrc.json │ ├── .vscode/ │ │ ├── extensions.json │ │ └── settings.json │ ├── env.d.ts │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── api/ │ │ │ ├── kernel.ts │ │ │ ├── request.ts │ │ │ └── websocket.ts │ │ ├── assets/ │ │ │ ├── globalMethods.ts │ │ │ ├── logo.ts │ │ │ ├── main.less │ │ │ ├── polyfills.ts │ │ │ └── styles/ │ │ │ ├── custom.less │ │ │ ├── reset.less │ │ │ ├── theme.less │ │ │ ├── utilities/ │ │ │ │ ├── display.less │ │ │ │ ├── flex.less │ │ │ │ ├── gap.less │ │ │ │ ├── grid.less │ │ │ │ ├── index.less │ │ │ │ ├── others.less │ │ │ │ ├── rounded.less │ │ │ │ ├── size.less │ │ │ │ ├── spacing.less │ │ │ │ └── text.less │ │ │ └── variables.less │ │ ├── bridge/ │ │ │ ├── app.ts │ │ │ ├── exec.ts │ │ │ ├── index.ts │ │ │ ├── io.ts │ │ │ ├── mmdb.ts │ │ │ ├── net.ts │ │ │ ├── notification.ts │ │ │ ├── server.ts │ │ │ └── wailsjs/ │ │ │ ├── go/ │ │ │ │ ├── bridge/ │ │ │ │ │ ├── App.d.ts │ │ │ │ │ └── App.js │ │ │ │ └── models.ts │ │ │ └── runtime/ │ │ │ ├── package.json │ │ │ ├── runtime.d.ts │ │ │ └── runtime.js │ │ ├── components/ │ │ │ ├── Button/ │ │ │ │ └── index.vue │ │ │ ├── Card/ │ │ │ │ └── index.vue │ │ │ ├── CheckBox/ │ │ │ │ └── index.vue │ │ │ ├── CodeViewer/ │ │ │ │ └── index.vue │ │ │ ├── ColorPicker/ │ │ │ │ └── index.vue │ │ │ ├── Confirm/ │ │ │ │ └── index.vue │ │ │ ├── CustomAction/ │ │ │ │ └── index.vue │ │ │ ├── Divider/ │ │ │ │ └── index.vue │ │ │ ├── Dropdown/ │ │ │ │ └── index.vue │ │ │ ├── Empty/ │ │ │ │ └── index.vue │ │ │ ├── Icon/ │ │ │ │ ├── icons.ts │ │ │ │ └── index.vue │ │ │ ├── Input/ │ │ │ │ └── index.vue │ │ │ ├── InputList/ │ │ │ │ └── index.vue │ │ │ ├── InterfaceSelect/ │ │ │ │ └── index.vue │ │ │ ├── KeyValueEditor/ │ │ │ │ └── index.vue │ │ │ ├── Menu/ │ │ │ │ └── index.vue │ │ │ ├── Message/ │ │ │ │ └── index.vue │ │ │ ├── Modal/ │ │ │ │ ├── index.ts │ │ │ │ └── index.vue │ │ │ ├── MultipleSelect/ │ │ │ │ └── index.vue │ │ │ ├── Pagination/ │ │ │ │ └── index.vue │ │ │ ├── Picker/ │ │ │ │ └── index.vue │ │ │ ├── Progress/ │ │ │ │ └── index.vue │ │ │ ├── Prompt/ │ │ │ │ └── index.vue │ │ │ ├── Radio/ │ │ │ │ └── index.vue │ │ │ ├── Select/ │ │ │ │ └── index.vue │ │ │ ├── Switch/ │ │ │ │ └── index.vue │ │ │ ├── Table/ │ │ │ │ └── index.vue │ │ │ ├── Tabs/ │ │ │ │ └── index.vue │ │ │ ├── Tag/ │ │ │ │ └── index.vue │ │ │ ├── Tips/ │ │ │ │ └── index.vue │ │ │ ├── TrafficChart/ │ │ │ │ └── index.vue │ │ │ ├── _common/ │ │ │ │ ├── AboutView.vue │ │ │ │ ├── CommandView.vue │ │ │ │ ├── NavigationBar.vue │ │ │ │ ├── SplashView.vue │ │ │ │ └── TitleBar.vue │ │ │ ├── components.d.ts │ │ │ └── index.ts │ │ ├── constant/ │ │ │ ├── app.ts │ │ │ ├── kernel.ts │ │ │ └── profile.ts │ │ ├── directives/ │ │ │ ├── index.ts │ │ │ ├── menu.ts │ │ │ ├── platform.ts │ │ │ └── tips.ts │ │ ├── enums/ │ │ │ ├── app.ts │ │ │ └── kernel.ts │ │ ├── hooks/ │ │ │ ├── index.ts │ │ │ ├── useBool.ts │ │ │ └── useCoreBranch.ts │ │ ├── lang/ │ │ │ ├── index.ts │ │ │ └── locale/ │ │ │ ├── en.ts │ │ │ └── zh.ts │ │ ├── main.ts │ │ ├── router/ │ │ │ ├── index.ts │ │ │ ├── router.d.ts │ │ │ └── routes.ts │ │ ├── stores/ │ │ │ ├── app.ts │ │ │ ├── appSettings.ts │ │ │ ├── env.ts │ │ │ ├── index.ts │ │ │ ├── kernelApi.ts │ │ │ ├── logs.ts │ │ │ ├── plugins.ts │ │ │ ├── profiles.ts │ │ │ ├── rulesets.ts │ │ │ ├── scheduledtasks.ts │ │ │ └── subscribes.ts │ │ ├── types/ │ │ │ ├── app.d.ts │ │ │ ├── global.d.ts │ │ │ ├── kernel.d.ts │ │ │ ├── profile.d.ts │ │ │ └── typescript.d.ts │ │ ├── utils/ │ │ │ ├── command.ts │ │ │ ├── completion.ts │ │ │ ├── env.ts │ │ │ ├── eventBus.ts │ │ │ ├── format.ts │ │ │ ├── generator.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ ├── interaction.ts │ │ │ ├── is.ts │ │ │ ├── migration.ts │ │ │ ├── others.ts │ │ │ ├── restorer.ts │ │ │ └── tray.ts │ │ └── views/ │ │ ├── HomeView/ │ │ │ ├── components/ │ │ │ │ ├── CommonController.vue │ │ │ │ ├── ConnectionsController.vue │ │ │ │ ├── GroupsController.vue │ │ │ │ ├── KernelLogs.vue │ │ │ │ ├── LogsController.vue │ │ │ │ ├── OverView.vue │ │ │ │ └── QuickStart.vue │ │ │ └── index.vue │ │ ├── PluginsView/ │ │ │ ├── components/ │ │ │ │ ├── PluginChangelog.vue │ │ │ │ ├── PluginConfigItem.vue │ │ │ │ ├── PluginConfigurator.vue │ │ │ │ ├── PluginForm.vue │ │ │ │ ├── PluginHub.vue │ │ │ │ └── PluginView.vue │ │ │ └── index.vue │ │ ├── ProfilesView/ │ │ │ ├── components/ │ │ │ │ ├── DnsConfig.vue │ │ │ │ ├── DnsRulesConfig.vue │ │ │ │ ├── DnsServersConfig.vue │ │ │ │ ├── GeneralConfig.vue │ │ │ │ ├── InboundsConfig.vue │ │ │ │ ├── MixinAndScriptConfig.vue │ │ │ │ ├── OutboundsConfig.vue │ │ │ │ ├── ProfileEditor.vue │ │ │ │ ├── ProfileForm.vue │ │ │ │ ├── RouteConfig.vue │ │ │ │ ├── RouteRulesConfig.vue │ │ │ │ └── RouteRulesetConfig.vue │ │ │ └── index.vue │ │ ├── RulesetsView/ │ │ │ ├── components/ │ │ │ │ ├── RulesetForm.vue │ │ │ │ ├── RulesetHub.vue │ │ │ │ └── RulesetView.vue │ │ │ └── index.vue │ │ ├── ScheduledTasksView/ │ │ │ ├── components/ │ │ │ │ ├── ScheduledTaskForm.vue │ │ │ │ └── ScheduledTasksLogs.vue │ │ │ └── index.vue │ │ ├── SettingsView/ │ │ │ ├── components/ │ │ │ │ ├── CoreSettings.vue │ │ │ │ ├── GeneralSettings.vue │ │ │ │ ├── PluginSettings.vue │ │ │ │ └── components/ │ │ │ │ ├── AdvancedSettings.vue │ │ │ │ ├── BehaviorSettings.vue │ │ │ │ ├── BranchDetail.vue │ │ │ │ ├── CoreConfig.vue │ │ │ │ ├── FeatureSettings.vue │ │ │ │ ├── PersonalizationSettings.vue │ │ │ │ ├── SwitchBranch.vue │ │ │ │ └── SystemProxySettings.vue │ │ │ └── index.vue │ │ └── SubscribesView/ │ │ ├── components/ │ │ │ ├── ProxiesEditor.vue │ │ │ ├── ProxiesView.vue │ │ │ ├── SubscribeForm.vue │ │ │ └── SubscribeScript.vue │ │ └── index.vue │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── go.mod ├── go.sum ├── main.go └── wails.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/release.yml ================================================ name: Build GUI.for.SingBox on: push: tags: - "v*" permissions: contents: write jobs: Build-Frontend: runs-on: ubuntu-latest if: github.repository == 'GUI-for-Cores/GUI.for.SingBox' steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 with: version: latest - uses: actions/setup-node@v6 with: node-version: "latest" cache: "pnpm" cache-dependency-path: frontend/pnpm-lock.yaml - run: | cd frontend pnpm install --frozen-lockfile pnpm build-only - uses: actions/upload-artifact@v6 with: name: frontend-dist path: frontend/dist Build-Windows: needs: Build-Frontend runs-on: windows-latest env: APP_NAME: GUI.for.SingBox steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - uses: actions/download-artifact@v8 with: name: frontend-dist path: frontend/dist - name: Build & Pack Windows shell: pwsh run: | function Build-And-Pack { param([string]$arch) $env:GOOS="windows" $env:GOARCH=$arch Write-Host "==> Building Windows $arch..." ~/go/bin/wails build -m -s -trimpath -skipbindings -devtools -tags webkit2_41 -o "$env:APP_NAME.exe" cd build/bin $zipName = "$env:APP_NAME-windows-$arch.zip" Compress-Archive -Path "$env:APP_NAME.exe" -DestinationPath $zipName -Force cd ../.. } $arches = @("amd64","arm64","386") foreach ($arch in $arches) { Build-And-Pack $arch } - uses: actions/upload-artifact@v6 with: name: windows-builds path: build/bin/*.zip Build-macOS: needs: Build-Frontend runs-on: macos-latest env: APP_NAME: GUI.for.SingBox steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - uses: actions/download-artifact@v8 with: name: frontend-dist path: frontend/dist - run: | go mod vendor sed -i "" "s/\[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular\]/[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]/g" vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/AppDelegate.m - name: Build & Pack macOS run: | build_and_pack() { arch=$1 export GOOS=darwin GOARCH=$arch echo "==> Building macOS $arch..." ~/go/bin/wails build -m -s -trimpath -skipbindings -devtools -tags webkit2_41 -o $APP_NAME.exe cd build/bin zip -q -r $APP_NAME-darwin-$arch.zip $APP_NAME.app cd ../.. } for arch in amd64 arm64; do build_and_pack $arch; done - uses: actions/upload-artifact@v6 with: name: macos-builds path: build/bin/*.zip Build-Linux: needs: Build-Frontend runs-on: ubuntu-latest env: APP_NAME: GUI.for.SingBox steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - uses: actions/download-artifact@v8 with: name: frontend-dist path: frontend/dist - run: | sudo apt-get update sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev - name: Build & Pack Linux run: | build_and_pack() { arch=$1 export GOOS=linux GOARCH=$arch echo "==> Building Linux $arch..." ~/go/bin/wails build -m -s -trimpath -skipbindings -devtools -tags webkit2_41 -o $APP_NAME.exe cd build/bin mv $APP_NAME.exe $APP_NAME zip $APP_NAME-linux-$arch.zip $APP_NAME cd ../.. } build_and_pack amd64 - uses: actions/upload-artifact@v6 with: name: linux-builds path: build/bin/*.zip Release: needs: [Build-Windows, Build-macOS, Build-Linux] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: path: release-assets - name: Create Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} files: release-assets/**/*.zip draft: false prerelease: ${{ contains(github.ref_name, 'dev') }} body: | Auto-generated release from GitHub Actions. ================================================ FILE: .github/workflows/rolling-release.yml ================================================ name: Rolling Release on: push: branches: [main] paths: - "frontend/**" workflow_dispatch: jobs: Build: permissions: write-all runs-on: ubuntu-latest if: github.repository == 'GUI-for-Cores/GUI.for.SingBox' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: latest - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "latest" cache: "pnpm" cache-dependency-path: frontend/pnpm-lock.yaml - name: Install dependencies run: cd frontend && pnpm install --frozen-lockfile - name: Build Frontend run: cd frontend && pnpm build-only - name: Create a compressed file run: | git rev-parse --short HEAD | tr -d '\n' > frontend/dist/version.txt cd frontend mv dist rolling-release zip -r rolling-release.zip rolling-release - name: Generate Changelog run: | set +e LAST_COMMIT=$(curl -L https://github.com/GUI-for-Cores/GUI.for.SingBox/releases/download/rolling-release/version.txt) echo -e "## Change log\n\n> Update time: $(TZ='Asia/Shanghai' date "+%Y-%m-%d %H:%M:%S")\n" > changelog.md git log $LAST_COMMIT..HEAD --pretty=format:"* %s" >> changelog.md if [ $? -ne 0 ]; then echo "No changes found since last commit." >> changelog.md fi set -e - name: Create Release and Upload Assets uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ./{frontend/{rolling-release.zip,rolling-release/version.txt},changelog.md} file_glob: true tag: rolling-release release_name: rolling-release overwrite: true draft: false prerelease: true body: | Rolling release built by GitHub Actions. To use this version, please install the "Rolling Release Assistant" plugin and enable "Enable Rolling Release" within the app. ================================================ FILE: .gitignore ================================================ build/bin frontend/dist .DS_Store ================================================ FILE: GUI.for.SingBox.code-workspace ================================================ { "folders": [ { "path": "." }, { "path": "frontend" } ], "settings": { "oxc.enable": true } } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================
GUI.for.SingBox

GUI.for.SingBox

A GUI program developed by vue3 + wails.

## Preview Take a look at the live version here: 👉 Live Demo
## Document [Community](https://gui-for-cores.github.io/guide/gfs/community) ## Build 1、Build Environment - Node.js [link](https://nodejs.org/en) - pnpm :`npm i -g pnpm` - Go [link](https://go.dev/) - Wails [link](https://wails.io/) :`go install github.com/wailsapp/wails/v2/cmd/wails@latest` 2、Pull and Build ```bash git clone https://github.com/GUI-for-Cores/GUI.for.SingBox.git cd GUI.for.SingBox/frontend pnpm install --frozen-lockfile && pnpm build cd .. wails build ``` ## Stargazers over time [![Stargazers over time](https://starchart.cc/GUI-for-Cores/GUI.for.SingBox.svg)](https://starchart.cc/GUI-for-Cores/GUI.for.SingBox) ================================================ FILE: bridge/bridge.go ================================================ package bridge import ( "embed" "log" "net" "os" "os/exec" "os/user" "path/filepath" "slices" "strings" sysruntime "runtime" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu/keys" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/runtime" "gopkg.in/yaml.v3" ) var Config = &AppConfig{} var Env = &EnvResult{ IsStartup: true, PreventExit: true, FromTaskSch: false, WebviewPath: "", AppName: "", AppVersion: "v1.21.0", BasePath: "", OS: sysruntime.GOOS, ARCH: sysruntime.GOARCH, IsPrivileged: false, } // NewApp creates a new App application struct func NewApp() *App { return &App{ AppMenu: menu.NewMenu(), } } func CreateApp(fs embed.FS) *App { exePath, err := os.Executable() if err != nil { panic(err) } Env.BasePath = filepath.ToSlash(filepath.Dir(exePath)) Env.AppName = filepath.Base(exePath) if slices.Contains(os.Args, "tasksch") { Env.FromTaskSch = true } if priv, err := IsPrivileged(); err == nil { Env.IsPrivileged = priv } app := NewApp() if Env.OS == "darwin" { createMacOSSymlink() createMacOSMenus(app) } if Env.OS == "windows" { processFixedWebView2Runtime() } extractEmbeddedFiles(fs) loadConfig() return app } func (a *App) IsStartup() bool { if Env.IsStartup { Env.IsStartup = false return true } return false } func (a *App) ExitApp() { log.Printf("ExitApp") Env.PreventExit = false runtime.Quit(a.Ctx) } func (a *App) RestartApp() FlagResult { log.Printf("RestartApp") exePath := Env.BasePath + "/" + Env.AppName cmd := exec.Command(exePath) SetCmdWindowHidden(cmd) if err := cmd.Start(); err != nil { return FlagResult{false, err.Error()} } a.ExitApp() return FlagResult{true, "Success"} } func (a *App) GetEnv() EnvResult { log.Printf("GetEnv") return EnvResult{ AppName: Env.AppName, AppVersion: Env.AppVersion, BasePath: Env.BasePath, OS: Env.OS, ARCH: Env.ARCH, IsPrivileged: Env.IsPrivileged, } } func (a *App) GetInterfaces() FlagResult { log.Printf("GetInterfaces") interfaces, err := net.Interfaces() if err != nil { return FlagResult{false, err.Error()} } var interfaceNames []string for _, inter := range interfaces { interfaceNames = append(interfaceNames, inter.Name) } return FlagResult{true, strings.Join(interfaceNames, "|")} } func (a *App) ShowMainWindow() { log.Printf("ShowMainWindow") runtime.WindowShow(a.Ctx) } func createMacOSSymlink() { user, _ := user.Current() linkPath := Env.BasePath + "/data" appPath := "/Users/" + user.Username + "/Library/Application Support/" + Env.AppName os.MkdirAll(appPath, os.ModePerm) os.Symlink(appPath, linkPath) } func createMacOSMenus(app *App) { appMenu := app.AppMenu.AddSubmenu("App") appMenu.AddText("Show", keys.CmdOrCtrl("s"), func(_ *menu.CallbackData) { runtime.WindowShow(app.Ctx) }) appMenu.AddText("Hide", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) { runtime.WindowHide(app.Ctx) }) appMenu.AddSeparator() appMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) { runtime.EventsEmit(app.Ctx, "onExitApp") }) // on macos platform, we should append EditMenu to enable Cmd+C,Cmd+V,Cmd+Z... shortcut app.AppMenu.Append(menu.EditMenu()) } func processFixedWebView2Runtime() { webviewDir := filepath.Join(Env.BasePath, "data", "WebView2") err := filepath.Walk(webviewDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if !info.IsDir() && strings.EqualFold(info.Name(), "msedgewebview2.exe") { Env.WebviewPath = filepath.Dir(path) log.Printf("WebView2 runtime already exists at: %s", Env.WebviewPath) return filepath.SkipDir } return nil }) if err != nil { log.Printf("Error during recursive search: %v\n", err) return } if Env.WebviewPath != "" { return } entries, err := os.ReadDir(webviewDir) if err != nil { log.Printf("Failed to read directory: %v\n", err) return } var cabFile string for _, e := range entries { if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".cab") && strings.Contains(e.Name(), "Microsoft.WebView2.FixedVersionRuntime") { cabFile = filepath.Join(webviewDir, e.Name()) break } } if cabFile == "" { log.Println("No WebView2 .cab file found. Skipping extraction.") return } log.Printf("Found CAB file: %s\n", cabFile) cmd := exec.Command("expand.exe", "-F:*", cabFile, webviewDir) SetCmdWindowHidden(cmd) log.Println("Extracting WebView2 Runtime...") if err := cmd.Run(); err != nil { log.Printf("Extraction failed: %v\n", err) return } log.Printf("WebView2 Runtime extracted successfully into: %s\n", webviewDir) Env.WebviewPath = strings.TrimSuffix(cabFile, ".cab") } func extractEmbeddedFiles(fs embed.FS) { iconSrc := "frontend/dist/icons" iconDst := "data/.cache/icons" imgSrc := "frontend/dist/imgs" imgDst := "data/.cache/imgs" os.MkdirAll(GetPath(iconDst), os.ModePerm) os.MkdirAll(GetPath(imgDst), os.ModePerm) extractFiles(fs, iconSrc, iconDst) extractFiles(fs, imgSrc, imgDst) } func extractFiles(fs embed.FS, srcDir, dstDir string) { files, _ := fs.ReadDir(srcDir) for _, file := range files { fileName := file.Name() dstPath := GetPath(dstDir + "/" + fileName) if _, err := os.Stat(dstPath); os.IsNotExist(err) { log.Printf("InitResources [%s]: %s", dstDir, fileName) data, _ := fs.ReadFile(srcDir + "/" + fileName) if err := os.WriteFile(dstPath, data, os.ModePerm); err != nil { log.Printf("Error writing file %s: %v", dstPath, err) } } } } func loadConfig() { b, err := os.ReadFile(Env.BasePath + "/data/user.yaml") if err == nil { yaml.Unmarshal(b, &Config) } if Config.Width == 0 { Config.Width = 800 } if Config.Height == 0 { Config.Height = 540 } Config.StartHidden = Env.FromTaskSch && Config.WindowStartState == int(options.Minimised) if !Env.FromTaskSch { Config.WindowStartState = int(options.Normal) } } ================================================ FILE: bridge/exec.go ================================================ package bridge import ( "bufio" "context" "fmt" "io" "log" "os" "os/exec" "strconv" "strings" "time" "github.com/shirou/gopsutil/v3/process" "github.com/wailsapp/wails/v2/pkg/runtime" ) func (a *App) Exec(path string, args []string, options ExecOptions) FlagResult { log.Printf("Exec: %s %s %v", path, args, options) exePath := GetPath(path) if _, err := os.Stat(exePath); os.IsNotExist(err) { exePath = path } cmd := exec.Command(exePath, args...) SetCmdWindowHidden(cmd) cmd.Dir = options.WorkingDirectory cmd.Env = os.Environ() for key, value := range options.Env { cmd.Env = append(cmd.Env, key+"="+value) } out, err := cmd.CombinedOutput() var output string if options.Convert { output = strings.TrimSpace(ConvertByte2String(out)) } else { output = strings.TrimSpace(string(out)) } if err != nil { if output == "" { output = err.Error() } return FlagResult{false, output} } return FlagResult{true, output} } func (a *App) ExecBackground(path string, args []string, outEvent string, endEvent string, options ExecOptions) FlagResult { log.Printf("ExecBackground: %s %s %s %s %v", path, args, outEvent, endEvent, options) exePath := GetPath(path) pidPath := "" if _, err := os.Stat(exePath); os.IsNotExist(err) { exePath = path } if options.PidFile != "" { pidPath = GetPath(options.PidFile) } cmd := exec.Command(exePath, args...) SetCmdWindowHidden(cmd) cmd.Dir = options.WorkingDirectory cmd.Env = os.Environ() for key, value := range options.Env { cmd.Env = append(cmd.Env, key+"="+value) } stdout, err := cmd.StdoutPipe() if err != nil { return FlagResult{false, err.Error()} } cmd.Stderr = cmd.Stdout if err := cmd.Start(); err != nil { return FlagResult{false, err.Error()} } pid := strconv.Itoa(cmd.Process.Pid) if pidPath != "" { err := os.WriteFile(pidPath, []byte(pid), os.ModePerm) if err != nil { _ = SendExitSignal(cmd.Process) _ = waitForProcessExitWithTimeout(cmd.Process, 10) return FlagResult{false, err.Error()} } } if outEvent != "" { scanAndEmit := func(reader io.Reader) { scanner := bufio.NewScanner(reader) stopOutput := false for scanner.Scan() { var text string if options.Convert { text = ConvertByte2String(scanner.Bytes()) } else { text = scanner.Text() } if !stopOutput { runtime.EventsEmit(a.Ctx, outEvent, text) if options.StopOutputKeyword != "" && strings.Contains(text, options.StopOutputKeyword) { stopOutput = true } } } } go scanAndEmit(stdout) } if endEvent != "" { go func() { cmd.Wait() if pidPath != "" { _ = os.Remove(pidPath) } runtime.EventsEmit(a.Ctx, endEvent) }() } return FlagResult{true, pid} } func (a *App) ProcessInfo(pid int32) FlagResult { log.Printf("ProcessInfo: %d", pid) proc, err := process.NewProcess(pid) if err != nil { return FlagResult{false, err.Error()} } name, err := proc.Name() if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, name} } func (a *App) ProcessMemory(pid int32) FlagResult { log.Printf("ProcessMemory: %d", pid) proc, err := process.NewProcess(pid) if err != nil { return FlagResult{false, err.Error()} } memInfo, err := proc.MemoryInfo() if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, strconv.FormatUint(memInfo.RSS, 10)} } func (a *App) KillProcess(pid int, timeout int) FlagResult { log.Printf("KillProcess: %d %d", pid, timeout) process, err := os.FindProcess(pid) if err != nil { return FlagResult{false, err.Error()} } if err := SendExitSignal(process); err != nil { log.Printf("SendExitSignal Err: %s", err.Error()) } if err := waitForProcessExitWithTimeout(process, timeout); err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func waitForProcessExitWithTimeout(process *os.Process, timeoutSeconds int) error { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second) defer cancel() interval := 10 * time.Millisecond maxInterval := 1000 * time.Millisecond for { select { case <-ctx.Done(): if killErr := process.Kill(); killErr != nil { return fmt.Errorf("timed out after %d seconds waiting for process %d, and failed to kill it: %w", timeoutSeconds, process.Pid, killErr) } return nil default: alive, err := IsProcessAlive(process) if err != nil { return fmt.Errorf("failed to check status of process %d: %w", process.Pid, err) } if !alive { return nil } time.Sleep(interval) interval = min(time.Duration(interval*2), maxInterval) } } } ================================================ FILE: bridge/exec_others.go ================================================ //go:build !windows package bridge import ( "errors" "fmt" "os" "os/exec" "syscall" ) func SetCmdWindowHidden(cmd *exec.Cmd) { } func SendExitSignal(p *os.Process) error { return p.Signal(syscall.SIGINT) } func IsProcessAlive(p *os.Process) (bool, error) { err := p.Signal(syscall.Signal(0)) if err == nil { return true, nil } if errors.Is(err, os.ErrProcessDone) { return false, nil } if errno, ok := err.(syscall.Errno); ok { switch errno { case syscall.ESRCH: return false, nil case syscall.EPERM: return true, nil } } return false, fmt.Errorf("failed to check process %d: %w", p.Pid, err) } func IsPrivileged() (bool, error) { return os.Geteuid() == 0, nil } ================================================ FILE: bridge/exec_windows.go ================================================ //go:build windows package bridge import ( "fmt" "os" "os/exec" "syscall" "unsafe" "golang.org/x/sys/windows" ) const ATTACH_PARENT_PROCESS uintptr = ^uintptr(0) var ( modAdvapi32 = windows.NewLazySystemDLL("advapi32.dll") modKernel32 = windows.NewLazySystemDLL("kernel32.dll") procCheckTokenMembership = modAdvapi32.NewProc("CheckTokenMembership") procFreeConsole = modKernel32.NewProc("FreeConsole") procAttachConsole = modKernel32.NewProc("AttachConsole") procSetConsoleCtrlHandler = modKernel32.NewProc("SetConsoleCtrlHandler") procGenerateConsoleCtrlEvent = modKernel32.NewProc("GenerateConsoleCtrlEvent") ) func SetCmdWindowHidden(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: windows.CREATE_UNICODE_ENVIRONMENT | windows.CREATE_NEW_PROCESS_GROUP, HideWindow: true, } } func SendExitSignal(p *os.Process) error { if ret, _, err := procFreeConsole.Call(); ret == 0 && err != windows.ERROR_INVALID_HANDLE { return err } defer func() { procAttachConsole.Call(ATTACH_PARENT_PROCESS) }() if ret, _, err := procAttachConsole.Call(uintptr(p.Pid)); ret == 0 && err != windows.ERROR_ACCESS_DENIED { return err } if ret, _, err := procSetConsoleCtrlHandler.Call(0, 1); ret == 0 { return err } if ret, _, err := procGenerateConsoleCtrlEvent.Call(windows.CTRL_BREAK_EVENT, uintptr(p.Pid)); ret == 0 { return err } return nil } func IsProcessAlive(p *os.Process) (bool, error) { h, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(p.Pid)) if err != nil { if err == windows.ERROR_INVALID_PARAMETER { return false, nil } return false, err } defer windows.CloseHandle(h) s, err := windows.WaitForSingleObject(h, 0) if err != nil { return false, err } switch s { case windows.WAIT_OBJECT_0: return false, nil case uint32(windows.WAIT_TIMEOUT): return true, nil default: return false, fmt.Errorf("unexpected WaitForSingleObject status: %d", s) } } func IsPrivileged() (bool, error) { var sid *windows.SID sid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) if err != nil { return false, err } var isMember int32 ret, _, err := procCheckTokenMembership.Call(0, uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(&isMember))) if ret == 0 { return false, err } return isMember != 0, nil } ================================================ FILE: bridge/io.go ================================================ package bridge import ( "archive/tar" "archive/zip" "compress/gzip" "encoding/base64" "fmt" "io" "log" "os" "path/filepath" "strings" "github.com/pkg/browser" ) const ( Binary = "Binary" Text = "Text" ) func (a *App) WriteFile(path string, content string, options IOOptions) FlagResult { log.Printf("WriteFile [%s %s]: %s", options.Mode, options.Range, path) fullPath := GetPath(path) if err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm); err != nil { return FlagResult{false, err.Error()} } var data []byte var err error switch options.Mode { case Text: data = []byte(content) case Binary: data, err = base64.StdEncoding.DecodeString(content) if err != nil { return FlagResult{false, err.Error()} } default: return FlagResult{false, "Unsupported IO mode: " + options.Mode} } file, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE, 0644) if err != nil { return FlagResult{false, err.Error()} } defer file.Close() stat, err := file.Stat() if err != nil { return FlagResult{false, err.Error()} } fileSize := stat.Size() var start, end int64 if options.Range == "" { start = 0 end = int64(len(data)) - 1 if err := file.Truncate(0); err != nil { return FlagResult{false, err.Error()} } } else { start, end, err = ParseRange(options.Range, fileSize) if err != nil { return FlagResult{false, err.Error()} } writeLength := int64(len(data)) if writeLength != end-start+1 { return FlagResult{false, "data length does not match range length"} } } _, err = file.WriteAt(data, start) if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) ReadFile(path string, options IOOptions) FlagResult { log.Printf("ReadFile [%s %s]: %s", options.Mode, options.Range, path) fullPath := GetPath(path) file, err := os.Open(fullPath) if err != nil { return FlagResult{false, err.Error()} } defer file.Close() stat, err := file.Stat() if err != nil { return FlagResult{false, err.Error()} } fileSize := stat.Size() start, end, err := ParseRange(options.Range, fileSize) if err != nil { return FlagResult{false, err.Error()} } length := end - start + 1 buf := make([]byte, length) n, err := file.ReadAt(buf, start) if err != nil && err != io.EOF { return FlagResult{false, err.Error()} } buf = buf[:n] switch options.Mode { case Text: return FlagResult{true, string(buf)} case Binary: return FlagResult{true, base64.StdEncoding.EncodeToString(buf)} default: return FlagResult{false, "Unsupported IO mode: " + options.Mode} } } func (a *App) MoveFile(source string, target string) FlagResult { log.Printf("MoveFile: %s -> %s", source, target) fullSource := GetPath(source) fullTarget := GetPath(target) if err := os.MkdirAll(filepath.Dir(fullTarget), os.ModePerm); err != nil { return FlagResult{false, err.Error()} } if err := os.Rename(fullSource, fullTarget); err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) RemoveFile(path string) FlagResult { log.Printf("RemoveFile: %s", path) fullPath := GetPath(path) if err := os.RemoveAll(fullPath); err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) CopyFile(src string, dst string) FlagResult { log.Printf("CopyFile: %s -> %s", src, dst) srcPath := GetPath(src) dstPath := GetPath(dst) srcFile, err := os.Open(srcPath) if err != nil { return FlagResult{false, err.Error()} } defer srcFile.Close() if err := os.MkdirAll(filepath.Dir(dstPath), os.ModePerm); err != nil { return FlagResult{false, err.Error()} } dstFile, err := os.Create(dstPath) if err != nil { return FlagResult{false, err.Error()} } defer dstFile.Close() if _, err := io.Copy(dstFile, srcFile); err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) MakeDir(path string) FlagResult { log.Printf("MakeDir: %s", path) fullPath := GetPath(path) if err := os.MkdirAll(fullPath, os.ModePerm); err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) ReadDir(path string) FlagResult { log.Printf("ReadDir: %s", path) fullPath := GetPath(path) files, err := os.ReadDir(fullPath) if err != nil { return FlagResult{false, err.Error()} } var result []string for _, file := range files { if info, err := file.Info(); err == nil { result = append(result, fmt.Sprintf("%v,%v,%v", info.Name(), info.Size(), info.IsDir())) } } return FlagResult{true, strings.Join(result, "|")} } func (a *App) OpenDir(path string) FlagResult { log.Printf("OpenDir: %s", path) fullPath := GetPath(path) err := browser.OpenURL(fullPath) if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) OpenURI(uri string) FlagResult { log.Printf("OpenURI: %s", uri) err := browser.OpenURL(uri) if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) AbsolutePath(path string) FlagResult { log.Printf("AbsolutePath: %s", path) absPath := GetPath(path) return FlagResult{true, absPath} } func (a *App) UnzipZIPFile(path string, output string) FlagResult { log.Printf("UnzipZIPFile: %s -> %s", path, output) fullPath := GetPath(path) outputPath := GetPath(output) archive, err := zip.OpenReader(fullPath) if err != nil { return FlagResult{false, err.Error()} } defer archive.Close() cleanOutputPath := outputPath + "/" for _, f := range archive.File { filePath := filepath.ToSlash(filepath.Clean(filepath.Join(outputPath, f.Name))) if !strings.HasPrefix(filePath, cleanOutputPath) { continue } if f.FileInfo().IsDir() { os.MkdirAll(filePath, os.ModePerm) continue } if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { continue } fileInArchive, err := f.Open() if err != nil { continue } dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { fileInArchive.Close() continue } if _, err := io.Copy(dstFile, fileInArchive); err != nil { fileInArchive.Close() dstFile.Close() continue } fileInArchive.Close() dstFile.Close() } return FlagResult{true, "Success"} } func (a *App) UnzipTarGZFile(path string, output string) FlagResult { log.Printf("UnzipTarGZFile: %s -> %s", path, output) fullPath := GetPath(path) outputPath := GetPath(output) gzipFile, err := os.Open(fullPath) if err != nil { return FlagResult{false, err.Error()} } defer gzipFile.Close() gzipReader, err := gzip.NewReader(gzipFile) if err != nil { return FlagResult{false, err.Error()} } defer gzipReader.Close() tarReader := tar.NewReader(gzipReader) cleanOutputPath := outputPath + "/" for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return FlagResult{false, err.Error()} } filePath := filepath.ToSlash(filepath.Clean(filepath.Join(outputPath, header.Name))) if !strings.HasPrefix(filePath, cleanOutputPath) { continue } if header.Typeflag == tar.TypeDir { os.MkdirAll(filePath, os.ModePerm) continue } if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { continue } dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode()) if err != nil { continue } if _, err := io.Copy(dstFile, tarReader); err != nil { dstFile.Close() continue } dstFile.Close() } return FlagResult{true, "Success"} } func (a *App) UnzipGZFile(path string, output string) FlagResult { log.Printf("UnzipGZFile: %s -> %s", path, output) fullPath := GetPath(path) outputPath := GetPath(output) gzipFile, err := os.Open(fullPath) if err != nil { return FlagResult{false, err.Error()} } defer gzipFile.Close() outputFile, err := os.Create(outputPath) if err != nil { return FlagResult{false, err.Error()} } defer outputFile.Close() gzipReader, err := gzip.NewReader(gzipFile) if err != nil { return FlagResult{false, err.Error()} } defer gzipReader.Close() if _, err := io.Copy(outputFile, gzipReader); err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } func (a *App) FileExists(path string) FlagResult { log.Printf("FileExists: %s", path) path = GetPath(path) _, err := os.Stat(path) if err == nil { return FlagResult{true, "true"} } if os.IsNotExist(err) { return FlagResult{true, "false"} } return FlagResult{false, err.Error()} } ================================================ FILE: bridge/mmdb.go ================================================ package bridge import ( "encoding/json" "errors" "log" "net" "sync" "github.com/oschwald/geoip2-golang" ) type MMDBInstance = struct { Refs map[string]bool Reader *geoip2.Reader } var ( mu sync.RWMutex mmdbMap = make(map[string]*MMDBInstance) ) func (a *App) OpenMMDB(path string, id string) FlagResult { log.Printf("OpenMMDB: %s -> %s", id, path) mu.Lock() defer mu.Unlock() if db, exists := mmdbMap[path]; exists { db.Refs[id] = true return FlagResult{true, "Success"} } reader, err := geoip2.Open(GetPath(path)) if err != nil { return FlagResult{false, "Failed to open mmdb: " + err.Error()} } mmdbMap[path] = &MMDBInstance{ Refs: map[string]bool{id: true}, Reader: reader, } return FlagResult{true, "Success"} } func (a *App) CloseMMDB(path string, id string) FlagResult { log.Printf("CloseMMDB: %s -> %s", id, path) mu.Lock() defer mu.Unlock() db, exists := mmdbMap[path] if !exists { return FlagResult{false, "Database not open: " + path} } if !db.Refs[id] { return FlagResult{false, "Reference not found for: " + id} } delete(db.Refs, id) if len(db.Refs) == 0 { if err := db.Reader.Close(); err != nil { return FlagResult{false, "Failed to close reader: " + err.Error()} } delete(mmdbMap, path) } return FlagResult{true, "Success"} } func (a *App) QueryMMDB(path string, ip string, dataType string) FlagResult { log.Printf("QueryMMDB: %s -> %s", path, ip) parsedIP := net.ParseIP(ip) if parsedIP == nil { return FlagResult{false, "Invalid IP address"} } mu.RLock() db, exists := mmdbMap[path] mu.RUnlock() if !exists { return FlagResult{false, "Database not open: " + path} } record, err := queryByType(db.Reader, parsedIP, dataType) if err != nil { return FlagResult{false, err.Error()} } bytes, err := json.Marshal(record) if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, string(bytes)} } func queryByType(reader *geoip2.Reader, ip net.IP, dataType string) (any, error) { switch dataType { case "ASN": return reader.ASN(ip) case "AnonymousIP": return reader.AnonymousIP(ip) case "City": return reader.City(ip) case "ConnectionType": return reader.ConnectionType(ip) case "Country": return reader.Country(ip) case "Domain": return reader.Domain(ip) case "Enterprise": return reader.Enterprise(ip) default: return nil, errors.New("Unsupported query type: " + dataType) } } ================================================ FILE: bridge/net.go ================================================ package bridge import ( "bytes" "context" "crypto/tls" "io" "log" "mime/multipart" "net/http" "os" "path/filepath" "strings" "github.com/wailsapp/wails/v2/pkg/runtime" ) func (a *App) Requests(method string, url string, headers map[string]string, body string, options RequestOptions) HTTPResult { log.Printf("Requests: %v %v %v %v %v", method, url, headers, body, options) client, ctx, cancel := withRequestOptionsClient(options) req, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(body)) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } req.Header = GetHeader(headers) if options.CancelId != "" { runtime.EventsOn(a.Ctx, options.CancelId, func(data ...any) { log.Printf("Requests Canceled: %v %v", method, url) cancel() }) defer runtime.EventsOff(a.Ctx, options.CancelId) } resp, err := client.Do(req) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } return HTTPResult{true, resp.StatusCode, resp.Header, string(b)} } func (a *App) Download(method string, url string, path string, headers map[string]string, event string, options RequestOptions) HTTPResult { log.Printf("Download: %s %s %s %v %s %v", method, url, path, headers, event, options) client, ctx, cancel := withRequestOptionsClient(options) req, err := http.NewRequestWithContext(ctx, method, url, nil) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } req.Header = GetHeader(headers) if options.CancelId != "" { runtime.EventsOn(a.Ctx, options.CancelId, func(data ...any) { log.Printf("Download Canceled: %v %v", url, path) cancel() }) defer runtime.EventsOff(a.Ctx, options.CancelId) } resp, err := client.Do(req) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } defer resp.Body.Close() path = GetPath(path) err = os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } file, err := os.Create(path) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } defer file.Close() reader := wrapWithProgress(resp.Body, resp.ContentLength, event, a) _, err = io.Copy(file, reader) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } return HTTPResult{true, resp.StatusCode, resp.Header, "Success"} } func (a *App) Upload(method string, url string, path string, headers map[string]string, event string, options RequestOptions) HTTPResult { log.Printf("Upload: %s %s %s %v %s %v", method, url, path, headers, event, options) path = GetPath(path) file, err := os.Open(path) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } defer file.Close() fileStat, err := file.Stat() if err != nil { return HTTPResult{false, 500, nil, err.Error()} } body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile(options.FileField, path) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } reader := wrapWithProgress(file, fileStat.Size(), event, a) _, err = io.Copy(part, reader) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } err = writer.Close() if err != nil { return HTTPResult{false, 500, nil, err.Error()} } client, ctx, cancel := withRequestOptionsClient(options) if options.CancelId != "" { runtime.EventsOn(a.Ctx, options.CancelId, func(data ...any) { log.Printf("Upload Canceled: %v %v", url, path) cancel() }) defer runtime.EventsOff(a.Ctx, options.CancelId) } req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } req.Header = GetHeader(headers) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := client.Do(req) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return HTTPResult{false, 500, nil, err.Error()} } return HTTPResult{true, resp.StatusCode, resp.Header, string(b)} } func (wt *WriteTracker) Write(p []byte) (n int, err error) { n = len(p) wt.Progress += int64(n) shouldEmit := wt.Total <= 0 || wt.Progress-wt.LastEmitted >= wt.EmitThreshold || wt.Progress == wt.Total if shouldEmit { runtime.EventsEmit(wt.App.Ctx, wt.ProgressChange, wt.Progress, wt.Total) wt.LastEmitted = wt.Progress } return n, nil } func wrapWithProgress(r io.Reader, size int64, event string, a *App) io.Reader { if event == "" { return r } return io.TeeReader(r, &WriteTracker{ Total: size, EmitThreshold: 128 * 1024, ProgressChange: event, App: a, }) } func withRequestOptionsClient(options RequestOptions) (*http.Client, context.Context, context.CancelFunc) { client := &http.Client{ Timeout: GetTimeout(options.Timeout), Transport: &http.Transport{ Proxy: GetProxy(options.Proxy), TLSClientConfig: &tls.Config{ InsecureSkipVerify: options.Insecure, }, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { if !options.Redirect { return http.ErrUseLastResponse } return nil }, } ctx, cancel := context.WithCancel(context.Background()) return client, ctx, cancel } ================================================ FILE: bridge/notification.go ================================================ package bridge import ( "github.com/gen2brain/beeep" ) func (a *App) Notify(title string, message string, icon string, options NotifyOptions) FlagResult { fullPath := GetPath(icon) beeep.AppName = options.AppName var err error if options.Beep { err = beeep.Alert(title, message, fullPath) } else { err = beeep.Notify(title, message, fullPath) } if err != nil { return FlagResult{false, err.Error()} } return FlagResult{true, "Success"} } ================================================ FILE: bridge/server.go ================================================ package bridge import ( "context" "crypto/tls" "encoding/base64" "encoding/json" "io" "log" "net" "net/http" "os" "path/filepath" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/wailsapp/wails/v2/pkg/runtime" ) var requestCounter uint64 var serverMap sync.Map type ResponseData struct { Status int Headers map[string]string Body string } func (a *App) StartServer(address string, serverID string, options ServerOptions) FlagResult { log.Printf("StartServer: %s %s %v", address, serverID, options) mux := http.NewServeMux() if options.StaticPath != "" && options.StaticRoute != "" { static := GetPath(options.StaticPath) fs := http.StripPrefix(options.StaticRoute, http.FileServer(http.Dir(static))) mux.HandleFunc(options.StaticRoute, func(w http.ResponseWriter, r *http.Request) { handleFileDownload(w, r, fs, options.StaticHeaders) }) } if options.UploadPath != "" && options.UploadRoute != "" { uploadPath := GetPath(options.UploadPath) if err := os.MkdirAll(uploadPath, os.ModePerm); err != nil { return FlagResult{false, "Failed to create upload directory: " + err.Error()} } maxUploadSize := options.MaxUploadSize if maxUploadSize <= 0 { maxUploadSize = 50 * 1024 * 1024 // 50MB } mux.HandleFunc(options.UploadRoute, func(w http.ResponseWriter, r *http.Request) { handleFileUpload(w, r, uploadPath, maxUploadSize, options.UploadHeaders) }) } var listener net.Listener if options.Cert != "" && options.Key != "" { cert, err := tls.LoadX509KeyPair(GetPath(options.Cert), GetPath(options.Key)) if err != nil { return FlagResult{false, "Failed to load TLS cert: " + err.Error()} } tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}} ln, err := net.Listen("tcp", address) if err != nil { return FlagResult{false, "Failed to bind address: " + err.Error()} } listener = tls.NewListener(ln, tlsConfig) } else { ln, err := net.Listen("tcp", address) if err != nil { return FlagResult{false, "Failed to bind address: " + err.Error()} } listener = ln } mux.HandleFunc("/", handleHttpRequest(a, serverID)) server := &http.Server{ Addr: address, Handler: mux, } go func() { if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { log.Printf("Server error on %s: %v", address, err) } }() serverMap.Store(serverID, server) return FlagResult{true, "Success"} } func (a *App) StopServer(id string) FlagResult { log.Printf("StopServer: %s", id) val, ok := serverMap.Load(id) if !ok { return FlagResult{false, "server not found"} } server, ok := val.(*http.Server) if !ok { return FlagResult{false, "invalid server type"} } err := server.Close() if err != nil { return FlagResult{false, err.Error()} } serverMap.Delete(id) return FlagResult{true, "Success"} } func (a *App) ListServer() FlagResult { log.Printf("ListServer") var servers []string serverMap.Range(func(key, value any) bool { serverID, ok := key.(string) if ok { servers = append(servers, serverID) } return true }) return FlagResult{true, strings.Join(servers, "|")} } func handleHttpRequest(a *App, serverID string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 20*1024*1024) // 20MB body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Failed to read request body: "+err.Error(), 500) return } count := atomic.AddUint64(&requestCounter, 1) requestID := serverID + strconv.FormatUint(count, 10) respChan := make(chan ResponseData, 1) ctx, cancel := context.WithTimeout(a.Ctx, 60*time.Second) // 60s defer cancel() runtime.EventsOn(ctx, requestID, func(data ...any) { defer runtime.EventsOff(ctx, requestID) resp := buildResponse(data) respChan <- resp }) runtime.EventsEmit(a.Ctx, serverID, requestID, r.Method, r.URL.RequestURI(), r.Header, body) select { case res := <-respChan: for k, v := range res.Headers { w.Header().Set(k, v) } w.WriteHeader(res.Status) w.Write([]byte(res.Body)) case <-ctx.Done(): http.Error(w, "Request timed out", http.StatusGatewayTimeout) } } } func buildResponse(data []any) ResponseData { resp := ResponseData{Status: 200, Headers: make(map[string]string), Body: "A sample http server"} if len(data) >= 4 { if status, ok := data[0].(float64); ok { resp.Status = int(status) } if headers, ok := data[1].(string); ok { json.Unmarshal([]byte(headers), &resp.Headers) } if body, ok := data[2].(string); ok { resp.Body = body } if optionsStr, ok := data[3].(string); ok { var ioOptions IOOptions json.Unmarshal([]byte(optionsStr), &ioOptions) if ioOptions.Mode == Binary { decoded, err := base64.StdEncoding.DecodeString(resp.Body) if err != nil { resp.Status = 500 resp.Body = err.Error() } else { resp.Body = string(decoded) } } } } return resp } func handleFileDownload(w http.ResponseWriter, r *http.Request, fs http.Handler, headers map[string]string) { for key, value := range headers { w.Header().Set(key, value) } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } fs.ServeHTTP(w, r) } func handleFileUpload(w http.ResponseWriter, r *http.Request, uploadPath string, maxUploadSize int64, headers map[string]string) { for key, value := range headers { w.Header().Set(key, value) } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } if r.Method != http.MethodPost && r.Method != http.MethodPut { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) contentType := r.Header.Get("Content-Type") if strings.HasPrefix(contentType, "multipart/form-data") { handleMultipartUpload(w, r, uploadPath) } else { handleRawUpload(w, r, uploadPath) } } func handleMultipartUpload(w http.ResponseWriter, r *http.Request, uploadPath string) { reader, err := r.MultipartReader() if err != nil { http.Error(w, "Invalid multipart form: "+err.Error(), http.StatusBadRequest) return } for { part, err := reader.NextPart() if err == io.EOF { break } if err != nil { http.Error(w, "Error reading upload stream: "+err.Error(), http.StatusInternalServerError) return } if part.FileName() == "" { part.Close() continue } dst, err := os.Create(filepath.Join(uploadPath, filepath.Base(part.FileName()))) if err != nil { http.Error(w, "Error creating file: "+err.Error(), http.StatusInternalServerError) return } if _, err = io.Copy(dst, part); err != nil { dst.Close() part.Close() http.Error(w, "Error saving file: "+err.Error(), http.StatusInternalServerError) return } dst.Close() part.Close() } w.Write([]byte("File uploaded successfully")) } func handleRawUpload(w http.ResponseWriter, r *http.Request, uploadPath string) { name := r.Header.Get("X-Filename") if name == "" { http.Error(w, "Missing X-Filename", 400) return } dst, err := os.Create(filepath.Join(uploadPath, filepath.Base(name))) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer dst.Close() if _, err := io.Copy(dst, r.Body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write([]byte("File uploaded successfully")) } ================================================ FILE: bridge/tray.go ================================================ package bridge import ( "log" "os" "github.com/energye/systray" "github.com/wailsapp/wails/v2/pkg/runtime" ) func CreateTray(a *App, icon []byte) (trayStart, trayEnd func()) { return systray.RunWithExternalLoop(func() { systray.SetIcon(icon) systray.SetTooltip("GUI.for.Cores") systray.SetOnRClick(func(menu systray.IMenu) { menu.ShowMenu() }) systray.SetOnClick(func(menu systray.IMenu) { if Env.OS == "darwin" { menu.ShowMenu() } else { a.ShowMainWindow() } }) addClickMenuItem := func(title, tooltip string, action func()) { m := systray.AddMenuItem(title, tooltip) m.Click(action) } // Ensure the tray is still available if rolling-release fails addClickMenuItem("Show", "Show", func() { a.ShowMainWindow() }) addClickMenuItem("Restart", "Restart", func() { a.RestartApp() }) addClickMenuItem("Exit", "Exit", func() { a.ExitApp() }) }, nil) } func (a *App) UpdateTray(tray TrayContent) { log.Printf("UpdateTray") updateTray(a, tray) } func (a *App) UpdateTrayMenus(menus []MenuItem) { log.Printf("UpdateTrayMenus") updateTrayMenus(a, menus) } func (a *App) UpdateTrayAndMenus(tray TrayContent, menus []MenuItem) { log.Printf("UpdateTrayAndMenus") updateTray(a, tray) updateTrayMenus(a, menus) } func createMenuItem(menu MenuItem, a *App, parent *systray.MenuItem) { if menu.Hidden { return } switch menu.Type { case "item": var m *systray.MenuItem if parent == nil { m = systray.AddMenuItem(menu.Text, menu.Tooltip) } else { m = parent.AddSubMenuItem(menu.Text, menu.Tooltip) } m.Click(func() { go runtime.EventsEmit(a.Ctx, "onMenuItemClick", menu.Event) }) if menu.Checked { m.Check() } for _, child := range menu.Children { createMenuItem(child, a, m) } case "separator": systray.AddSeparator() } } func updateTray(a *App, tray TrayContent) { if tray.Icon != "" { ico, err := os.ReadFile(GetPath(tray.Icon)) if err == nil { systray.SetIcon(ico) } } if tray.Title != "" { systray.SetTitle(tray.Title) runtime.WindowSetTitle(a.Ctx, tray.Title) } if tray.Tooltip != "" { systray.SetTooltip(tray.Tooltip) } } func updateTrayMenus(a *App, menus []MenuItem) { systray.ResetMenu() for _, menu := range menus { createMenuItem(menu, a, nil) } } ================================================ FILE: bridge/types.go ================================================ package bridge import ( "context" "net/http" "github.com/wailsapp/wails/v2/pkg/menu" ) // App struct type App struct { Ctx context.Context AppMenu *menu.Menu } type EnvResult struct { IsStartup bool `json:"-"` PreventExit bool `json:"-"` FromTaskSch bool `json:"-"` WebviewPath string `json:"-"` AppName string `json:"appName"` AppVersion string `json:"appVersion"` BasePath string `json:"basePath"` OS string `json:"os"` ARCH string `json:"arch"` IsPrivileged bool `json:"isPrivileged"` } type RequestOptions struct { Proxy string Insecure bool Redirect bool Timeout int CancelId string FileField string } type ExecOptions struct { PidFile string StopOutputKeyword string WorkingDirectory string Convert bool Env map[string]string } type Range struct { Start *int64 End *int64 } type IOOptions struct { Mode string // Binary / Text Range string // "start-end" / "start-" / "-end" } type FlagResult struct { Flag bool `json:"flag"` Data string `json:"data"` } type ServerOptions struct { Cert string Key string StaticPath string StaticRoute string StaticHeaders map[string]string UploadPath string UploadRoute string UploadHeaders map[string]string MaxUploadSize int64 } type NotifyOptions struct { AppName string Beep bool } type HTTPResult struct { Flag bool `json:"flag"` Status int `json:"status"` Headers http.Header `json:"headers"` Body string `json:"body"` } type AppConfig struct { WindowStartState int `yaml:"windowStartState"` WebviewGpuPolicy int `yaml:"webviewGpuPolicy"` Width int `yaml:"width"` Height int `yaml:"height"` MultipleInstance bool `yaml:"multipleInstance"` RollingRelease bool `yaml:"rollingRelease" default:"true"` StartHidden bool } type TrayContent struct { Icon string `json:"icon,omitempty"` Title string `json:"title,omitempty"` Tooltip string `json:"tooltip,omitempty"` } type WriteTracker struct { Total int64 Progress int64 LastEmitted int64 EmitThreshold int64 ProgressChange string App *App } type MenuItem struct { Type string `json:"type"` // Menu Type: item / separator Text string `json:"text"` Tooltip string `json:"tooltip"` Event string `json:"event"` Children []MenuItem `json:"children"` Hidden bool `json:"hidden"` Checked bool `json:"checked"` } ================================================ FILE: bridge/utils.go ================================================ package bridge import ( "errors" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" "golang.org/x/text/encoding/simplifiedchinese" ) func GetPath(path string) string { if !filepath.IsAbs(path) { path = filepath.Join(Env.BasePath, path) } return filepath.ToSlash(filepath.Clean(path)) } func GetProxy(_proxy string) func(*http.Request) (*url.URL, error) { proxy := http.ProxyFromEnvironment if _proxy != "" { proxyUrl, err := url.Parse(_proxy) if err == nil { proxy = http.ProxyURL(proxyUrl) } } return proxy } func GetTimeout(timeout int) time.Duration { if timeout <= 0 { return 15 * time.Second } return time.Duration(timeout) * time.Second } func GetHeader(headers map[string]string) http.Header { header := make(http.Header, len(headers)) for key, value := range headers { header.Set(key, value) } return header } func ConvertByte2String(byte []byte) string { decodeBytes, _ := simplifiedchinese.GB18030.NewDecoder().Bytes(byte) return string(decodeBytes) } func ParseRange(s string, size int64) (start int64, end int64, err error) { if s == "" { return 0, size - 1, nil } s = strings.TrimSpace(s) // "bytes=100-200" s = strings.TrimPrefix(s, "bytes=") parts := strings.SplitN(s, "-", 2) if len(parts) != 2 { return 0, 0, errors.New("invalid range format") } startStr := strings.TrimSpace(parts[0]) endStr := strings.TrimSpace(parts[1]) // "-200" last 200 bytes if startStr == "" && endStr != "" { e, err2 := strconv.ParseInt(endStr, 10, 64) if err2 != nil || e < 0 { return 0, 0, errors.New("invalid range value") } if e > size { start = 0 } else { start = size - e } end = size - 1 return start, end, nil } // "100-" from start to EOF if startStr != "" && endStr == "" { start, err = strconv.ParseInt(startStr, 10, 64) if err != nil || start < 0 { return 0, 0, errors.New("invalid range value") } end = size - 1 return start, end, nil } // "100-200" if startStr != "" && endStr != "" { start, err = strconv.ParseInt(startStr, 10, 64) if err != nil || start < 0 { return 0, 0, errors.New("invalid range value") } end, err = strconv.ParseInt(endStr, 10, 64) if err != nil || end < 0 { return 0, 0, errors.New("invalid range value") } if start > end { return 0, 0, errors.New("invalid range: start > end") } if end >= size { end = size - 1 } return start, end, nil } return 0, 0, errors.New("invalid range format") } func RollingRelease(next http.Handler) http.Handler { isDevVersion := strings.Contains(Env.AppVersion, "dev") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { url := r.URL.Path isIndex := url == "/" if isIndex { w.Header().Set("Cache-Control", "no-cache") } else { w.Header().Set("Cache-Control", "max-age=31536000, immutable") } if isDevVersion || !Config.RollingRelease { next.ServeHTTP(w, r) return } if isIndex { url = "/index.html" } filePath := GetPath("data/rolling-release" + url) if _, err := os.Stat(filePath); err != nil { next.ServeHTTP(w, r) return } http.ServeFile(w, r, filePath) }) } ================================================ FILE: build/README.md ================================================ # Build Directory The build directory is used to house all the build files and assets for your application. The structure is: * bin - Output directory * darwin - macOS specific files * windows - Windows specific files ## Mac The `darwin` directory holds files specific to Mac builds. These may be customised and used as part of the build. To return these files to the default state, simply delete them and build with `wails build`. The directory contains the following files: - `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. - `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. ## Windows The `windows` directory contains the manifest and rc files used when building with `wails build`. These may be customised for your application. To return these files to the default state, simply delete them and build with `wails build`. - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file will be created using the `appicon.png` file in the build directory. - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, as well as the application itself (right click the exe -> properties -> details) - `wails.exe.manifest` - The main application manifest file. ================================================ FILE: build/darwin/Info.dev.plist ================================================ CFBundlePackageType APPL CFBundleName {{.Info.ProductName}} CFBundleExecutable {{.Name}} CFBundleIdentifier com.wails.{{.Name}} CFBundleVersion {{.Info.ProductVersion}} CFBundleGetInfoString {{.Info.Comments}} CFBundleShortVersionString {{.Info.ProductVersion}} CFBundleIconFile iconfile LSMinimumSystemVersion 10.13.0 NSHighResolutionCapable true NSHumanReadableCopyright {{.Info.Copyright}} {{if .Info.FileAssociations}} CFBundleDocumentTypes {{range .Info.FileAssociations}} CFBundleTypeExtensions {{.Ext}} CFBundleTypeName {{.Name}} CFBundleTypeRole {{.Role}} CFBundleTypeIconFile {{.IconName}} {{end}} {{end}} {{if .Info.Protocols}} CFBundleURLTypes {{range .Info.Protocols}} CFBundleURLName com.wails.{{.Scheme}} CFBundleURLSchemes {{.Scheme}} CFBundleTypeRole {{.Role}} {{end}} {{end}} NSAppTransportSecurity NSAllowsLocalNetworking ================================================ FILE: build/darwin/Info.plist ================================================ CFBundlePackageType APPL CFBundleName {{.Info.ProductName}} CFBundleExecutable {{.Name}} CFBundleIdentifier com.wails.{{.Name}} CFBundleVersion {{.Info.ProductVersion}} CFBundleGetInfoString {{.Info.Comments}} CFBundleShortVersionString {{.Info.ProductVersion}} CFBundleIconFile iconfile LSMinimumSystemVersion 10.13.0 NSHighResolutionCapable true NSHumanReadableCopyright {{.Info.Copyright}} LSUIElement true {{if .Info.FileAssociations}} CFBundleDocumentTypes {{range .Info.FileAssociations}} CFBundleTypeExtensions {{.Ext}} CFBundleTypeName {{.Name}} CFBundleTypeRole {{.Role}} CFBundleTypeIconFile {{.IconName}} {{end}} {{end}} {{if .Info.Protocols}} CFBundleURLTypes {{range .Info.Protocols}} CFBundleURLName com.wails.{{.Scheme}} CFBundleURLSchemes {{.Scheme}} CFBundleTypeRole {{.Role}} {{end}} {{end}} ================================================ FILE: build/windows/info.json ================================================ { "fixed": { "file_version": "{{.Info.ProductVersion}}" }, "info": { "0000": { "ProductVersion": "{{.Info.ProductVersion}}", "CompanyName": "{{.Info.CompanyName}}", "FileDescription": "{{.Info.ProductName}}", "LegalCopyright": "{{.Info.Copyright}}", "ProductName": "{{.Info.ProductName}}", "Comments": "{{.Info.Comments}}" } } } ================================================ FILE: build/windows/wails.exe.manifest ================================================ true/pm permonitorv2,permonitor ================================================ FILE: frontend/.editorconfig ================================================ [*.{ts,vue,less}] charset = utf-8 indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf max_line_length = 100 ================================================ FILE: frontend/.gitattributes ================================================ * text=auto eol=lf ================================================ FILE: frontend/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/extensions.json !.vscode/settings.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? *.tsbuildinfo package.json.md5 .eslintcache ================================================ FILE: frontend/.oxfmtrc.json ================================================ { "$schema": "./node_modules/oxfmt/configuration_schema.json", "semi": false, "tabWidth": 2, "singleQuote": true, "printWidth": 100, "trailingComma": "all", "ignorePatterns": ["src/bridge/wailsjs/**"] } ================================================ FILE: frontend/.oxlintrc.json ================================================ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"], "env": { "browser": true }, "categories": { "correctness": "error" }, "rules": { "no-unused-expressions": [ "error", { "allowShortCircuit": true, "allowTernary": true } ] } } ================================================ FILE: frontend/.vscode/extensions.json ================================================ { "recommendations": [ "Vue.volar", "dbaeumer.vscode-eslint", "EditorConfig.EditorConfig", "oxc.oxc-vscode" ] } ================================================ FILE: frontend/.vscode/settings.json ================================================ { "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { "tsconfig.json": "tsconfig.*.json, env.d.ts", "package.json": "package-lock.json, pnpm*, .oxlint*, eslint*, .oxfmtrc*, .editorconfig" }, "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, "editor.formatOnSave": true, "editor.defaultFormatter": "oxc.oxc-vscode" } ================================================ FILE: frontend/env.d.ts ================================================ /// interface ImportMetaEnv { readonly VITE_APP_TITLE: string readonly VITE_APP_VERSION: string readonly VITE_APP_PROJECT_URL: string readonly VITE_APP_LOCALES_URL: string readonly VITE_APP_TG_GROUP: string readonly VITE_APP_TG_CHANNEL: string readonly VITE_APP_VERSION_API: string } interface ImportMeta { readonly env: ImportMetaEnv } ================================================ FILE: frontend/eslint.config.js ================================================ import skipFormatting from 'eslint-config-prettier/flat' import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' import { globalIgnores } from 'eslint/config' import pluginOxlint from 'eslint-plugin-oxlint' import pluginVue from 'eslint-plugin-vue' export default defineConfigWithVueTs( { name: 'app/files-to-lint', files: ['**/*.{ts,vue}'], }, globalIgnores(['**/dist/**', '**/wailsjs/**']), ...pluginVue.configs['flat/recommended'], vueTsConfigs.recommended, skipFormatting, ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'), { rules: { '@typescript-eslint/no-explicit-any': ['off'], 'vue/no-v-html': ['off'], 'vue/multi-word-component-names': [ 'error', { ignores: ['index'], }, ], }, }, ) ================================================ FILE: frontend/index.html ================================================ %VITE_APP_TITLE%
================================================ FILE: frontend/package.json ================================================ { "name": "frontend", "version": "0.0.0", "private": true, "type": "module", "scripts": { "dev": "vite --host", "build": "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", "lint": "run-s lint:*", "lint:oxlint": "oxlint . --fix", "lint:eslint": "eslint . --fix --cache", "format": "oxfmt src/" }, "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.2", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/lint": "^6.9.5", "@codemirror/merge": "^6.12.0", "@codemirror/state": "^6.5.4", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.16", "codemirror": "^6.0.2", "croner": "10.0.1", "marked": "^17.0.4", "pinia": "^3.0.4", "prettier": "^3.8.1", "vue": "^3.5.29", "vue-draggable-plus": "^0.6.1", "vue-i18n": "^11.2.8", "vue-router": "^5.0.3", "yaml": "^2.8.2" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", "@types/node": "^25.3.5", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.9.0", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-oxlint": "^1.51.0", "eslint-plugin-vue": "^10.8.0", "less": "^4.5.1", "npm-run-all2": "^8.0.4", "oxfmt": "^0.36.0", "oxlint": "^1.51.0", "typescript": "~5.9.3", "vite": "8.0.0-beta.16", "vue-tsc": "^3.2.5" } } ================================================ FILE: frontend/src/App.vue ================================================ ================================================ FILE: frontend/src/api/kernel.ts ================================================ import { Request } from '@/api/request' import { WebSockets } from '@/api/websocket' import { useProfilesStore } from '@/stores' import type { CoreApiConfig, CoreApiProxies, CoreApiConnections, CoreApiWsDataMap, } from '@/types/kernel' type WsKey = keyof CoreApiWsDataMap type WsChannel = { url: string params?: Recordable handlers: Array<(data: CoreApiWsDataMap[K]) => void> isActive: boolean connect?: () => void disconnect?: () => void } export enum Api { Configs = '/configs', Memory = '/memory', Proxies = '/proxies', ProxyDelay = '/proxies/{0}/delay', Connections = '/connections', Traffic = '/traffic', Logs = '/logs', } const setupCoreApi = (protocol: 'http' | 'ws') => { const { currentProfile: profile } = useProfilesStore() let base = `${protocol}://127.0.0.1:20123` let bearer = '' if (profile) { const controller = profile.experimental.clash_api.external_controller || '127.0.0.1:20123' const [, port = 20123] = controller.split(':') base = `${protocol}://127.0.0.1:${port}` bearer = profile.experimental.clash_api.secret } if (protocol === 'http') { request.base = base request.bearer = bearer } else { websocket.base = base websocket.bearer = bearer } } const request = new Request({ beforeRequest: () => setupCoreApi('http'), timeout: 60 * 1000 }) const websocket = new WebSockets({ beforeConnect: () => setupCoreApi('ws') }) const wsChannels: { [K in WsKey]: WsChannel } = { logs: { url: Api.Logs, isActive: false, handlers: [], params: { level: 'debug' } }, memory: { url: Api.Memory, isActive: false, handlers: [] }, traffic: { url: Api.Traffic, isActive: false, handlers: [] }, connections: { url: Api.Connections, isActive: false, handlers: [] }, } const createCoreWSHandlerRegister = (key: K) => { const channel = wsChannels[key] return (cb: (data: CoreApiWsDataMap[K]) => void) => { channel.handlers.push(cb) if (!channel.isActive && channel.connect) { channel.connect() channel.isActive = true } const unregister = () => { const idx = channel.handlers.indexOf(cb) idx !== -1 && channel.handlers.splice(idx, 1) if (channel.isActive && channel.disconnect && channel.handlers.length === 0) { channel.disconnect() channel.isActive = false } } return unregister } } // restful api export const getConfigs = () => request.get(Api.Configs) export const setConfigs = (body = {}) => request.patch(Api.Configs, body) export const getProxies = () => request.get(Api.Proxies) export const getConnections = () => request.get(Api.Connections) export const deleteConnection = (id: string) => request.delete(Api.Connections + '/' + id) export const useProxy = (group: string, proxy: string) => { return request.put(Api.Proxies + '/' + group, { name: proxy }) } export const getProxyDelay = (proxy: string, url: string, timeout: number) => { return request.get>(Api.ProxyDelay.replace('{0}', proxy), { url, timeout, }) } // websocket api export const onLogs = createCoreWSHandlerRegister('logs') export const onMemory = createCoreWSHandlerRegister('memory') export const onTraffic = createCoreWSHandlerRegister('traffic') export const onConnections = createCoreWSHandlerRegister('connections') export const initWebsocket = () => { Object.values(wsChannels).forEach((channel) => { const { connect, disconnect } = websocket.createWS({ url: channel.url, params: channel.params, cb: (data) => channel.handlers.forEach((cb) => cb(data)), }) channel.connect = connect channel.disconnect = disconnect channel.isActive = false if (channel.handlers.length > 0) { channel.connect() channel.isActive = true } }) } export const destroyWebsocket = () => { Object.values(wsChannels).forEach((channel) => { channel.disconnect?.() channel.connect = undefined channel.disconnect = undefined channel.isActive = false }) } ================================================ FILE: frontend/src/api/request.ts ================================================ import { parse } from 'yaml' type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' enum ResponseType { JSON = 'JSON', TEXT = 'TEXT', YAML = 'YAML', } type RequestOptions = { base?: string bearer?: string timeout?: number responseType?: ResponseType beforeRequest?: () => void } export class Request { public base: string public bearer: string public timeout: number public responseType: string public beforeRequest: () => void constructor(options: RequestOptions = {}) { this.base = options.base || '' this.bearer = options.bearer || '' this.timeout = options.timeout || 10000 this.responseType = options.responseType || ResponseType.JSON this.beforeRequest = options.beforeRequest || (() => 0) } private request = async ( url: string, options: { method: Method; body?: Record }, ) => { this.beforeRequest() const init: RequestInit = { method: options.method, signal: AbortSignal.timeout(this.timeout), } if (this.base) { url = this.base + url } if (this.bearer) { if (!init.headers) init.headers = {} Object.assign(init.headers, { Authorization: `Bearer ${this.bearer}` }) } if (['GET'].includes(options.method)) { const query = new URLSearchParams(options.body || {}).toString() query && (url += '?' + query) } if (['POST', 'PUT', 'PATCH'].includes(options.method)) { init.body = JSON.stringify(options.body || {}) } const res = await fetch(url, init) if (res.status === 204) { return null as T } if ([504, 401, 503].includes(res.status)) { const { message } = await res.json() throw message } if (this.responseType === ResponseType.TEXT) { const text = await res.text() return text as T } if (this.responseType === ResponseType.YAML) { const text = await res.text() return parse(text) as T } const json = await res.json() return json as T } public get = (url: string, body = {}) => this.request(url, { method: 'GET', body }) public post = (url: string, body = {}) => this.request(url, { method: 'POST', body }) public put = (url: string, body = {}) => this.request(url, { method: 'PUT', body }) public patch = (url: string, body = {}) => this.request(url, { method: 'PATCH', body }) public delete = (url: string) => this.request(url, { method: 'DELETE' }) } ================================================ FILE: frontend/src/api/websocket.ts ================================================ type WebSocketsOptions = { base?: string bearer?: string beforeConnect?: () => void } type Options = { url: string; cb: (data: any) => void; params?: Record } export class WebSockets { public base: string public bearer: string public beforeConnect: () => void constructor(options: WebSocketsOptions) { this.base = options.base || '' this.bearer = options.bearer || '' this.beforeConnect = options.beforeConnect || (() => 0) } public createWS(options: Options) { this.beforeConnect() const params = { ...options.params, token: this.bearer } const query = new URLSearchParams(params).toString() const url = query ? `${options.url}?${query}` : options.url let isManualClose = false let ws: WebSocket | null = null const connect = () => { ws = new WebSocket(this.base + url) ws.onmessage = (e) => options.cb(JSON.parse(e.data)) ws.onclose = () => { setTimeout(() => { if (!isManualClose) { setTimeout(connect, 3000) } }, 1000) } } const disconnect = () => { isManualClose = true if (ws) { ws.onmessage = null ws.onclose = null ws.close() ws = null } } return { connect, disconnect } } } ================================================ FILE: frontend/src/assets/globalMethods.ts ================================================ import * as Vue from 'vue' import { stringify, parse } from 'yaml' import * as Bridge from '@/bridge' import * as Stores from '@/stores' import * as Utils from '@/utils' /** * Expose methods to be used by the plugin system */ window.Plugins = { ...Bridge, ...Utils, ...Stores, YAML: { parse, stringify, }, } window.Vue = Vue window.AsyncFunction = Object.getPrototypeOf(async function () {}).constructor ================================================ FILE: frontend/src/assets/logo.ts ================================================ export default `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACKCAYAAABipUKtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAABMrSURBVHhe7Z0JVBRX1sddRmM0jhNHk3GS0Uy2yWYSRRZXBKEBge6mF8QVFMWNbgRUaDBBRY3mMxqXGLfBJRojJsYd950oSnAh4o57HLNMvpnvTJKJCe+7t+jWFm9DN/26qeque87veDynl+r7/99br169V9TzhkjKyWmqNWbG6wymfJ3RdElrMP0E/Ab//wH+/VJrzFoQm5IVptfrG5rfIoeHRH1dStYgrSHrHzpjFqsRg+mCNtmkMb9XDilHnNH0JFT7BlLomlkbMyL9CfNHySG1gErWQVv/hhDWEe7oUrL15o+UQwqhS81pCcKvIsSsNTBG+AQM1dr8FXKINbTJWREg1k1KRKfBMQR0FfNXySGmUI4b1xyEXwTn+wpSPI7AYHKN3A1EFCBGIFTnZUosV4FXFNqUCfKVQl2GLjX1Ua3R9B6c73+lRHIDFWC81X3T0lqZD0kOd0WMMdMXhD9DiOJ2wIS34d8Y86HJ4crQ63Mag/CTgF+qClHHVOCVh3p05h/NhyoH74g1ml6DJBcTyRcPBtPXsSkmtfmQ5XA2GGP1F6/bOmzKgpVwrjf9RCZdfODY4CO5GzgZCz8raLNy0+4tx766yC7f+oat2LCD9R87kUq4WLkFVwsq88+Rw5FYsq5As72w+JtLN++wa//4/h5ohsxZC6lkixUYG5hW4uyk+afJUV0sys9vsX73F3knzl+psBbemvJb37LVW/awgeMnUwkXJWCCW7rRmUrzz5SDik92HOhReKLs8pWvvyWFr0px2WU24f0lTJ+STSZdhFRoDaYVutRUuRtYx7Zt2x45UFw6vezKrbuU0NVRDmb5pGA/i8/MpRIuSvBehcaQFW3++d4dW/cfb19yrrzE3qq3RcnZcpYzL0863cBgwrHBsn4jMx83p8K7Ai7vGuwtOp0Gg7ofy8pvsauEqI6C3WDdjoNssGkqnXRxckNrzIwyp8U7Yteh4raHT5zdg+dwCyfPX2WXbn5DCusoJ85dYZM+WCatbmDIylOl5PzBnCLPjX3HTg8o+uriP63FtwbGAezqbVpYR8Bu8NmuQ2xI1jQ66aLEdCPWmNnbnCrPivW7iv5YePLs2uNnLpHCW4Pd4DKnboCflbtgBYuVzJUCrmcw/d2jusHe4tKwotILNymxq+MsdgNCVEfBAeb63YUsMfsdIuHiBAaI13F1kzmF0oxNxcVNC0+dmwdV/xslsD0I3eAWn25w6sJVNnXhShY7ZgKZdNEhjA0yl+ozMlqYUyqd2F9S6nu09MJZStTagGMDSlRHufL1d2zD3i/Y0AkS6gaGrGs6Q2a4ObXij91FJxKOnbn0CyWkM/DsBqcvXmNTF30kqW4A/y7ub8j5vTnN4gvcUqVLNqXHjsn+ad7q9cyeAV9t4NYNbn/HNu47At1gOp10EYLdQGvIVphTLp5QG7KfA5cesD7Y1Hfmsj1Fp0gRnYVvN7jO3lm8SjrdoPKewiJxdAPG6kPVD4eD+neVgxTA+/fLN+wgReQBXilQojoKdoNN+46yYW9JpxvA5eJVINSshPsjxjD+abhcKaAP7kEmzFnCDpWcIUV0Fp7zBqWXrrPpS1azPlK6UjBmLQw3GNzaDerrDNn9oQ19Tx6UDRJMU1j+9gOkiDwQZhEJUR3lKnSDzfuLWNJbM8jfIUZgbHAlNjk7xKyP6wJ3y8IXrqt6APaC8/PTYPR95NR5UkRncUU3kMrYAAoSn3XwIe6SMsvFNzTJJjU4zb499jWQ9PYMOOceIUXkAc9usOWAxLqB0VQO9DLL5nzgvDQ4azl8ONd9d1hZs1euE9b5USI6C99ucIPNWPqxpLqB1pi5wOlugKNMYSaK+BJeGKbMZjsKvyRF5AHXsYHEuoGwZzI5O9gsp/0xID29GXzAfOG8Qn0wZ/qmvc0Wrt3ssskjnt3gKxwbSKwbwL/z9aNyHjPLW33EJpu6whvOW3+Iuxj77gds77HTpIg84D02GCaxbqAxmILMMj8cSUmLGmkNmTPgfH+X/AA3MWDcJLZi405SQB5w7QaXb8CVgtTGBqYpOIFnlr0y9Pr8hlpj1kbqTXUFTh4ddNHkESJ0A6hkSlhHuN8NpDSLmD3eLH1lwGAvhX5h3YKTR2u37ycF5AF2g6q7j2pLZTeQyryB6T8P7F2EkX6dnPPtASePcCHHF6fOkSLyoOzKTa7dQAp3GM3PQqw8FVAvEBs44MLFHJSAPODdDYQ7jCJeixiZkJwM0j8qmCBm1DjyRWID2+t7y9eyo6UXSBF5wLMbbN5/VLRrEf0Vyqkg/p+BBvUU/YeRLxIrybmz2LZDxaSAPDjB+UphGnQDMe1TUA1PY+27Bs8F8Z8RDBAQEcOUSWnki8VKXNrbbMGajS6bSkaEXUscu4EY9inEjBzHsODbdwmad88A/mEq1rm3hkUOMZBvEjNp0+ez3UdPkgLyoHJswO8O4xQY0NZVN4gelsJC+w0VeMgAFoJjE5gmOZP8ALEyYNxElrd+u8umkhGes4gbYTDrzj2MmtEZLHzQyHviV2sApGuUnkUPHUN+mJjJmr2Y7S/+ihSQBzyvFLAbTF6wnOmJ38ETZVKq0PKtxa/RAJWoWUjfRJw+JD9YrCRkTmGrt+5hxwkBecHzSuHzPYXCMVO/xRm00MUjEkY/JLwFOwxQSTdlH2HUSH2JWMFz7KQPlrPDJ8pIAXnA854C7lOYOD+P/C21QTU8nSkGJJHCW7DbAEhAuFr4QJ1RWt0AZ+Q+3XmIFJAXlfcUaGEdAbsB7mgelFH7Zx9ht44cnEwKXhWHDGChh7ovU48cS365WMHJI1zN48qpZKEbcNzDiDfCqN9SHahL2MDhpNgUtTIAgnMGOKKkDkLMjJw4U1j7TwnIC+wGlKiOgt1gbcE+4dY49VseAKo+KtFIilwdtTaAhZ6aASxm1Hj6oEQKTh7N+egzl04l8+wGJefKWeZ7tp+LiPkPHziCFLgmnDYA0rm3lvWGcw51cGImZdocVnDYdesQEV67lrAbrNq8i/VLz3ngN+BleihxeWcvXAxgIVgfL0w2WB+g2MFta4vyN7MiF04ln4TzOa9uUFx2iWXMXCDkOSL+wUmd2sDVAEiXKB2LGppCJlvMjJ/5oUunkr88C93g6tekqI6CTztJnTyTFNRRuBvAQkjcEGESgkq2WBmUkStsYnXljaVTF64Jj7mlhLWHcugk2FFm/f1jUlBHcZkBkG7RscIUJJVssYKTR2/NXcr2Hy8lBeTBl2fL2bladIPz124L78XPmJ0nAQMgwuQRrjOQ2FQy3rL9eOtel95YOnURuoEdT0fFbet4z8D6vZIxgIXu6jimHpFOJlus4OTRlIUfuWxLO4IVjZVNCY9cvHEHLgOvPPQ+yRkAESaP4HqVSraYGZ7zLvt012GXdoPSi9eFwZ1FeLzlXFZ+k3wtIkkDWAjU9GdSWX9oASePZuatZYUnz5KC8AANho+4tQz0qNdYkLQBEFx51DthNJlsMWPIne2SqeR9MOhcvmEnW7xuG9ty4Bj5GmskbwALQfpBkpw8mrf6cy4PuMCq37j3KAi/lS3Kr2TJpwU1nm48xgBIl0idcCODSraYSZ8xnxUcrv2q5EMlZWzlpt33hLeAXaCmmUmPMoCFXn0GS2/yaPxkoXodubGE1b314HG2BISuKr5XGwDpGq1nymHSWoeIk0e4DtGeqeTCk+fYx1v3kcJb8GoDCISrhYOT2jrEwVlT2YqNu2xOJe8oLGFLPysgRbdGNoCZ7qo4YV0blWyxgpNHuA7Reir56OkLLH/7QVJsCtkAVuBUMi5xopItZvAJaPgMYnxMbt76HaTQtpANQNAjph9Tj5TW5NHgjFxS4JqQDWCDzhEaFpEwiky22IiGgSyu1Jm/egMpcnXYY4D3l60hBXUUSRnAQk/dQBYzWpzrECtX6oy6l2BXGWB2nhcbAOkSqRXdJlbl8LSHtl+5ygDvLlr5wPfUFskawAJuYq3rySP8/t6D6e1XrjKA6d155Pc5iuQNgAibWOto8kg9Ymy1269cYYBjZy6x2FFjye9zFI8wgAV3bmIVtl/BKYhKqjWuMMDS/E3kd9UGjzIA4o5NrPh0DXu3X/E2wPbDx5kqsWbj2YvHGQC5v4mVFrD2mIQl71QibcHTAFv2H2G6Eank99QWjzSABZ6bWDVw2Rk+yPHtV7wMsGjN5ywy4f7lJS882gCIsA4x3rlNrHh3knq6hj04a4ADxaUsNZfPJhAKjzeAhZ5axzexapIfnNSpDc4YYPXmnUyTNIb8XF54jQEQnErGSrZn3sDWM3UcxVEDLFy7hY2dNke4zufx/TXhVQawYM9updqc7ynmrfqcFJpi+uLV3K7v7UU2gA14GWDWsnxSbGsWrNnEknNmuKXiqyIbwAa8DDBx7lJSdAu5HyxnMVYPbnQ3sgFswMsAiRmTSOHx1DDMlMsUxHvciWwAG/AyALb17FkL7wsPg0Ic5EXZuHnkbqoYQPlfKmGehjsNYAHbvG5EOgur4bl97ua1gMD37xtAobxNJczTqAsDiJUXOwbkgvhtBQP4KVSHqIR5GrIB7vOnds+PBPHxD0bUr+cXppxJJczTkA1QSa8+Cf/XsGHDKBC/8g9H+YWpulMJ8zRkA1QSEBFzEGTvDjQTDABRH04DJVTSPAnZAEhixV9eeDkLNH8F+J2gPoZvqLKHn0L5K5U4T0E2wFDcfofVHwa0Ah78C6K+CpUBEvVb1cR5Ct5ugO7quAtNmjXrD1L/DWgkiF414FSghk5wlUqg1PFWA4T0HXIXOvzOJk2a9QWJXweaCmLbCp+kpEZwZTAO+JFKpFTxRgN0VfY53bLN0waQNQR4AUDxH2z9tsI3LPpVuEI4SiVTiniTAXrFDf7x1S498+ByTw1SYtW3BO4P+uwNHx8f6AZqE5wafqaSKiW8xQDdVX3OtGrT1gjy9QRwtq8JYF/V24qACFV7/zBlMZVYqeDpBgiJG/JT+y7BK6DqY0CyN4DHgYaoH5fw8YGxgUL5tr9CmjePPNkAMMI/98RT7caATEFAO8D5qrcVnXopO0A3OEElWcx4ogF6xQ35+fVuwaug6vHPwb8J8K16W/GKXt8YusFk6Aa/UMkWI55mgO7qfhee+Mtf00AOrHq8teu6qrcVviHKTnC5eJpKuNjwFAPAuf6XN3qErIGq14IEHQAc4bu+6m1FYGBgE3+FahpcMt6lEi8WPMEAPWL6Xn7ymWfHQdp7AVj1jwLurXpbEaBQ+4ERzlDJFwNSNgBWfYdARX7Dho11kOqOAN7Kdfy63tURGBjfxD9U+T9ivLEkVQP00PS/0ubZ5zMgvVj1fwXEU/W2orNC1QW6wTlKiLpCagYI6Zt4t0PPsPUNGzfWQ0p9AHFWva3wiYpqCpeLs+FKQRR3GKVkgEBN/+tPPfeSCdKIc/jPAvbP4YsthLUGYaqLlCjuRAoGgKr/tWNwxMZGjRr1gdR1AvC+vXSq3laEhoY2g24wry67gdgNEKgZcPPpF16egOkCngOkW/W2AgaHQSBGeVVx3IFYDRDSD6s+ckujJk3iIEW+QGtA+lVvK7oolc1hgPihu7uBGA3QUzvwVtuXXsuBtCiA5wHPq3pb4a+IDnXn6iMxGQDO9b91CoksgKrvB6nwAzy76m2FT4i+BZhgCQhUUVUw3ojFAEHagbfbvdx+Evx8XJyJq3RwebZ3VL2t8A9VR8Ap4QYlHC/q2gBY9b4h0TvMCzOx6p8A6MWZ3hjdIiMfh26wDMRySTeoSwP01A2688wrr0+BnxkOyFVfXQSEqqL9wlS3KBGdoS4MAFVfAabe3aRp84Hw0/yBJwG56muKAIWuJZhgFQjHrRu42wBBukHfPvtqh2nwc7DqcS3+Y0ADQA57wzdcGQNjAy7b191mAKz6cOW+ps2bD4KfEADIVe9MvBke3tovTPkJJaojuMMAQfr47557veMMOOwIQK56ngFVpQcj3KHEtQfXGiCxwj9cfaBp8xYJcKidgT8BjfG45eAY/sHRT4IJPqUErglXGQCq/p/Pv9FpJhxeb+AloDkgV70Lo75fWHRff4XqW0poW7jAAFD1MYebt2g5GI6pC9AGkKveXdExLKYNDBA3UGJT8DRAcGzCDy+86TsLDiMSeBmQq76OArqBaiCcFr6nRLeGlwECemu+eKxly0T4brnqxRIBkZFPwSlhCyW8BWcNEBQb/78vdvDDx6vh83XwCRty1Yss6sMpYbB/mPIH3gbo3Ft75LGWrbDquwL4dC2senkqV4zhExLVFoywnYcB4Fz/rxc7+s+Fj7VU/e8BueolEA38FdFJcFr41z0D2PEn5sIG3P+jUFD1RS0ebzUMPqsbIFe9FAOEf8ZPodqFBqjpr5Hjn3+trPr4f/+tU2d8lm40IFe99COnARggoXOk9lp1fz4mIn7UXf9w9V6o+qHwJqz6p4BHALnqPSHw+QahcUODtcasdTqj6Wez8BUxI8ed9wtXzflD69bx8LJgAOfw5ar35Ag3zHlEMyq7nXr0aNxtg2JjteN5vgUg37nzwsA274Wtvl69/wfOcO4gRpbr1QAAAABJRU5ErkJggg==` ================================================ FILE: frontend/src/assets/main.less ================================================ @import 'styles/variables.less'; @import 'styles/theme.less'; @import 'styles/reset.less'; @import 'styles/custom.less'; @import 'styles/utilities/index.less'; ================================================ FILE: frontend/src/assets/polyfills.ts ================================================ // Polyfill for Promise.withResolvers() if (typeof Promise.withResolvers !== 'function') { Promise.withResolvers = function () { let resolve!: (value: T | PromiseLike) => void let reject!: (reason?: any) => void const promise = new Promise((res, rej) => { resolve = res reject = rej }) return { promise, resolve, reject } } } ================================================ FILE: frontend/src/assets/styles/custom.less ================================================ body[feature-outline='true'] { * { outline: 1px solid var(--color); } } body[feature-no-animation='true'] { * { transition: none !important; } } body[feature-no-rounded='true'] { .rounded-2, .rounded-4, .rounded-6, .rounded-8, .rounded-16, .rounded-32, .rounded-9999, .rounded-full { border-radius: 0; } ::-webkit-scrollbar-track { border-radius: 0; } ::-webkit-scrollbar-thumb { border-radius: 0; } } body[feature-border='true'] { box-shadow: inset 0 0 1px var(--color); } body { margin: 0; color: var(--color); background-color: var(--bg-color); user-select: none; -webkit-user-select: none; line-height: 1.32; } #app { height: 100vh; display: flex; flex-direction: column; } .grid-list-grid { flex: 1; margin-top: 8px; overflow-y: auto; font-size: 12px; .grid-list-item { display: inline-block; margin: 8px; width: calc(100% / 3 - 16px); } } .grid-list-list { flex: 1; margin-top: 8px; overflow-y: auto; font-size: 12px; display: flex; flex-direction: column; gap: 16px; padding: 8px; } .grid-list-header { display: flex; align-items: center; padding: 0 8px; } .grid-list-empty { height: 90%; } .form-item { font-size: 14px; padding: 4px 12px; display: flex; align-items: center; justify-content: space-between; } .form-action { display: flex; justify-content: flex-end; margin-top: 8px; } .rotation { animation: rotate 1s infinite linear; } .hover\:\!bg-red { &:hover { background-color: rgba(255, 0, 0, 0.6) !important; } } @keyframes clip { from { clip-path: circle(0% at var(--x) var(--y)); } to { clip-path: circle(100% at var(--x) var(--y)); } } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .slide-down-enter-active, .slide-down-leave-active { transition: transform 0.2s ease-out; } .slide-down-enter-from, .slide-down-leave-to { transform: translateY(-100%); } ================================================ FILE: frontend/src/assets/styles/reset.less ================================================ div, input { box-sizing: border-box; } input { font-family: inherit; } a { text-decoration: none; color: var(--color); } ::view-transition-old(root) { animation: none; } ::view-transition-new(root) { mix-blend-mode: normal; animation: clip 0.5s ease-in; } ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { border-radius: 6px; background: var(--scrollbar-track-bg); } ::-webkit-scrollbar-thumb { border-radius: 6px; background: var(--scrollbar-thumb-bg); } ================================================ FILE: frontend/src/assets/styles/theme.less ================================================ :root { body[theme-mode='light'] { .set-theme(light); } body[theme-mode='dark'] { .set-theme(dark); } } .set-theme(@theme) { --color: ~'var(--color-@{theme})'; --bg-color: ~'var(--bg-color-@{theme})'; // Scrollbar --scrollbar-track-bg: ~'var(--scrollbar-track-bg-@{theme})'; --scrollbar-thumb-bg: ~'var(--scrollbar-thumb-bg-@{theme})'; // Button --btn-normal-color: ~'var(--btn-normal-color-@{theme})'; --btn-normal-bg: ~'var(--btn-normal-bg-@{theme})'; --btn-normal-hover-color: ~'var(--btn-normal-hover-color-@{theme})'; --btn-normal-hover-border-color: ~'var(--btn-normal-hover-border-color-@{theme})'; --btn-normal-active-color: ~'var(--btn-normal-active-color-@{theme})'; --btn-normal-active-border-color: ~'var(--btn-normal-active-border-color-@{theme})'; --btn-primary-color: ~'var(--btn-primary-color-@{theme})'; --btn-primary-bg: ~'var(--btn-primary-bg-@{theme})'; --btn-primary-hover-bg: ~'var(--btn-primary-hover-bg-@{theme})'; --btn-primary-active-bg: ~'var(--btn-primary-active-bg-@{theme})'; --btn-link-color: ~'var(--btn-link-color-@{theme})'; --btn-link-bg: ~'var(--btn-link-bg-@{theme})'; --btn-link-hover-color: ~'var(--btn-link-hover-color-@{theme})'; --btn-link-active-color: ~'var(--btn-link-active-color-@{theme})'; --btn-text-color: ~'var(--btn-text-color-@{theme})'; --btn-text-bg: ~'var(--btn-text-bg-@{theme})'; --btn-text-hover-bg: ~'var(--btn-text-hover-bg-@{theme})'; --btn-text-active-bg: ~'var(--btn-text-active-bg-@{theme})'; // Radio --radio-normal-color: ~'var(--radio-normal-color-@{theme})'; --radio-normal-bg: ~'var(--radio-normal-bg-@{theme})'; --radio-normal-hover-color: ~'var(--radio-normal-hover-color-@{theme})'; --radio-primary-color: ~'var(--radio-primary-color-@{theme})'; --radio-primary-bg: ~'var(--radio-primary-bg-@{theme})'; --radio-primary-hover-bg: ~'var(--radio-primary-hover-bg-@{theme})'; --radio-primary-active-bg: ~'var(--radio-primary-active-bg-@{theme})'; // Card --card-color: ~'var(--card-color-@{theme})'; --card-bg: ~'var(--card-bg-@{theme})'; --card-hover-bg: ~'var(--card-hover-bg-@{theme})'; --card-active-bg: ~'var(--card-active-bg-@{theme})'; // Progress --progress-bg: ~'var(--progress-bg-@{theme})'; --progress-inner-bg: ~'var(--progress-inner-bg-@{theme})'; // Dropdown --dropdown-bg: ~'var(--dropdown-bg-@{theme})'; // Modal --modal-bg: ~'var(--modal-bg-@{theme})'; --modal-mask-bg: ~'var(--modal-mask-bg-@{theme})'; // Switch --switch-on-bg: ~'var(--switch-on-bg-@{theme})'; --switch-on-hover-bg: ~'var(--switch-on-hover-bg-@{theme})'; --switch-on-dot-bg: ~'var(--switch-on-dot-bg-@{theme})'; --switch-off-bg: ~'var(--switch-off-bg-@{theme})'; --switch-off-hover-bg: ~'var(--switch-off-hover-bg-@{theme})'; --switch-off-dot-bg: ~'var(--switch-off-dot-bg-@{theme})'; // Input --input-color: ~'var(--input-color-@{theme})'; --input-bg: ~'var(--input-bg-@{theme})'; // ColorPicker --color-picker-bg: ~'var(--color-picker-bg-@{theme})'; // Divider --divider-color: ~'var(--divider-color-@{theme})'; // Select --select-bg: ~'var(--select-bg-@{theme})'; // Toast --toast-bg: ~'var(--toast-bg-@{theme})'; // Menu --menu-bg: ~'var(--menu-bg-@{theme})'; // Table --table-tr-odd-bg: ~'var(--table-tr-odd-bg-@{theme})'; --table-tr-even-bg: ~'var(--table-tr-even-bg-@{theme})'; --table-tr-odd-hover-bg: ~'var(--table-tr-odd-hover-bg-@{theme})'; --table-tr-even-hover-bg: ~'var(--table-tr-even-hover-bg-@{theme})'; } ================================================ FILE: frontend/src/assets/styles/utilities/display.less ================================================ .block { display: block; } .inline-block { display: inline-block; } .inline { display: inline; } .flex { display: flex; } .inline-flex { display: inline-flex; } .grid { display: grid; } .hidden { display: none; } .\!hidden { display: none !important; } .invisible { visibility: hidden; } ================================================ FILE: frontend/src/assets/styles/utilities/flex.less ================================================ .flex-row { flex-direction: row; } .flex-row-reverse { flex-direction: row-reverse; } .flex-col { flex-direction: column; } .flex-col-reverse { flex-direction: column-reverse; } .flex-wrap { flex-wrap: wrap; } .items-start { align-items: flex-start; } .items-center { align-items: center; } .items-end { align-items: flex-end; } .items-stretch { align-items: stretch; } .self-stretch { align-self: stretch; } .justify-start { justify-content: flex-start; } .justify-center { justify-content: center; } .justify-end { justify-content: flex-end; } .justify-between { justify-content: space-between; } .justify-around { justify-content: space-around; } .justify-evenly { justify-content: space-evenly; } .shrink-0 { flex-shrink: 0; } .flex-1 { flex: 1 1 0%; } .flex-none { flex: none; } .flex-auto { flex: 1 1 auto; } ================================================ FILE: frontend/src/assets/styles/utilities/gap.less ================================================ @gap-values: 0, 2, 4, 8, 10, 12, 16, 24, 32; .generate-gaps(@i: 1) when (@i <= length(@gap-values)) { @v: extract(@gap-values, @i); .gap-@{v} { gap: unit(@v, px); } .gap-x-@{v} { column-gap: unit(@v, px); } .gap-y-@{v} { row-gap: unit(@v, px); } .generate-gaps(@i + 1); } .generate-gaps(); ================================================ FILE: frontend/src/assets/styles/utilities/grid.less ================================================ @cols: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24, 32, 64; .generate-cols(@i: 1) when (@i <= length(@cols)) { @n: extract(@cols, @i); .grid-cols-@{n} { grid-template-columns: repeat(@n, minmax(0, 1fr)); } .col-span-@{n} { grid-column: span @n / span @n; } .generate-cols(@i + 1); } .generate-cols(); ================================================ FILE: frontend/src/assets/styles/utilities/index.less ================================================ @import 'spacing.less'; @import 'gap.less'; @import 'text.less'; @import 'display.less'; @import 'flex.less'; @import 'grid.less'; @import 'rounded.less'; @import 'size.less'; @import 'others.less'; ================================================ FILE: frontend/src/assets/styles/utilities/others.less ================================================ .fixed { position: fixed; } .sticky { position: sticky; } .relative { position: relative; } .absolute { position: absolute; } .inset-0 { inset: 0; } .z-2 { z-index: 2; } .z-3 { z-index: 3; } .z-9 { z-index: 9; } .z-99 { z-index: 99; } .z-999 { z-index: 999; } .z-9999 { z-index: 9999; } .top-0 { top: 0; } .right-8 { right: 8px; } .right-32 { right: 32px; } .bottom-4 { bottom: 4px; } .bottom-12 { bottom: 12px; } .bottom-32 { bottom: 32px; } .left-8 { left: 8px; } .left-1\/2 { left: 50%; } .backdrop-blur-sm { backdrop-filter: blur(4px); } .blur-3xl { filter: blur(64px); } .origin-center { transform-origin: center; } .rotate-0 { transform: rotate(0deg); } .-rotate-90 { transform: rotate(-90deg); } .rotate-90 { transform: rotate(90deg); } .rotate-180 { transform: rotate(180deg); } .-translate-x-1\/2 { transform: translateX(-50%); } .translate-y-0 { transform: translateY(0); } .translate-y-full { transform: translateY(100%); } .cursor-pointer { cursor: pointer; } .cursor-move { cursor: move; } .cursor-not-allowed { cursor: not-allowed; } .pointer-events-none { pointer-events: none; } .overflow-auto { overflow: auto; } .overflow-y-auto { overflow-y: auto; } .overflow-hidden { overflow: hidden; } .transition-all { transition-property: all; } .duration-100 { transition-duration: 0.1s; } .duration-200 { transition-duration: 0.2s; } .duration-400 { transition-duration: 0.4s; } .shadow { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .outline-none { outline: none; } .border-0 { border-width: 0; } .bg-transparent { background: transparent; } .border-collapse { border-collapse: collapse; } ================================================ FILE: frontend/src/assets/styles/utilities/rounded.less ================================================ @radius-values: 0, 2, 4, 6, 8, 16, 32, 9999; .generate-rounded(@i: 1) when (@i <= length(@radius-values)) { @r: extract(@radius-values, @i); @name: ~'@{r}'; .rounded-@{name} { border-radius: unit(@r, px); } .generate-rounded(@i + 1); } .generate-rounded(); .rounded-full { border-radius: 9999px; } ================================================ FILE: frontend/src/assets/styles/utilities/size.less ================================================ @size-values: 0, 8, 10, 12, 16, 18, 24, 26, 30, 32, 42, 64, 128, 256; @percent-values: 25, 36, 50, 60, 75, 90, 100; .generate-size(@i: 1) when (@i <= length(@size-values)) { @v: extract(@size-values, @i); .w-@{v} { width: unit(@v, px); } .h-@{v} { height: unit(@v, px); } .min-w-@{v} { min-width: unit(@v, px); } .min-h-@{v} { min-height: unit(@v, px); } .generate-size(@i + 1); } .generate-size(); .generate-percent(@i: 1) when (@i <= length(@percent-values)) { @p: extract(@percent-values, @i); .w-\[@{p}\%\] { width: ~'@{p}%'; } .h-\[@{p}\%\] { height: ~'@{p}%'; } .min-w-\[@{p}\%\] { min-width: ~'@{p}%'; } .min-h-\[@{p}\%\] { min-height: ~'@{p}%'; } .max-w-\[@{p}\%\] { max-width: ~'@{p}%'; } .max-h-\[@{p}\%\] { max-height: ~'@{p}%'; } .generate-percent(@i + 1); } .generate-percent(); .w-full { width: 100%; } .h-full { height: 100%; } ================================================ FILE: frontend/src/assets/styles/utilities/spacing.less ================================================ @spacing-values: 0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 36; @spacing-types: m, p; @directions: '', t, r, b, l, x, y; .generate-spacing(@typeIndex: 1) when (@typeIndex <= length(@spacing-types)) { @type: extract(@spacing-types, @typeIndex); .generate-direction(@dirIndex: 1) when (@dirIndex <= length(@directions)) { @dir: extract(@directions, @dirIndex); .generate-value(@valIndex: 1) when (@valIndex <= length(@spacing-values)) { @val: extract(@spacing-values, @valIndex); @class: ~'@{type}@{dir}-@{val}'; .@{class} { .apply-spacing(@type, @dir, unit(@val, px)); } .generate-value(@valIndex + 1); } .generate-auto() when (@type = m) { @class: ~'@{type}@{dir}-auto'; .@{class} { .apply-spacing(@type, @dir, auto); } } .generate-value(); .generate-auto(); .generate-direction(@dirIndex + 1); } .generate-direction(); .generate-spacing(@typeIndex + 1); } .generate-spacing(); .apply-spacing(@type, '', @value) when (@type = m) { margin: @value; } .apply-spacing(@type, '', @value) when (@type = p) { padding: @value; } .apply-spacing(@type, t, @value) when (@type = m) { margin-top: @value; } .apply-spacing(@type, t, @value) when (@type = p) { padding-top: @value; } .apply-spacing(@type, r, @value) when (@type = m) { margin-right: @value; } .apply-spacing(@type, r, @value) when (@type = p) { padding-right: @value; } .apply-spacing(@type, b, @value) when (@type = m) { margin-bottom: @value; } .apply-spacing(@type, b, @value) when (@type = p) { padding-bottom: @value; } .apply-spacing(@type, l, @value) when (@type = m) { margin-left: @value; } .apply-spacing(@type, l, @value) when (@type = p) { padding-left: @value; } .apply-spacing(@type, x, @value) when (@type = m) { margin-left: @value; margin-right: @value; } .apply-spacing(@type, x, @value) when (@type = p) { padding-left: @value; padding-right: @value; } .apply-spacing(@type, y, @value) when (@type = m) { margin-top: @value; margin-bottom: @value; } .apply-spacing(@type, y, @value) when (@type = p) { padding-top: @value; padding-bottom: @value; } ================================================ FILE: frontend/src/assets/styles/utilities/text.less ================================================ @text-sizes: 10, 12, 14, 16, 18, 20, 24, 32; .generate-text(@i: 1) when (@i <= length(@text-sizes)) { @s: extract(@text-sizes, @i); .text-@{s} { font-size: unit(@s, px); } .generate-text(@i + 1); } .generate-text(); .text-left { text-align: left; } .text-center { text-align: center; } .text-right { text-align: right; } .align-middle { vertical-align: middle; } .line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; } .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .underline { text-decoration: underline; } .text-nowrap { text-wrap: nowrap; } .text-ellipsis { text-overflow: ellipsis; } .break-all { word-break: break-all; } .break-keep { word-break: keep-all; } .whitespace-nowrap { white-space: nowrap; } .whitespace-pre-wrap { white-space: pre-wrap; } .leading-relaxed { line-height: 1.625; } .italic { font-style: italic; } .font-bold { font-weight: bold; } .font-normal { font-weight: normal; } .select-text { user-select: text; -webkit-user-select: text; } ================================================ FILE: frontend/src/assets/styles/variables.less ================================================ :root { --x: 0px; --y: 0px; --primary-color: rgb(0, 89, 214); --secondary-color: rgb(5, 62, 142); // Color & BackgroundColor --color-light: #000; --bg-color-light: rgba(246, 246, 246, 0.85); --color-dark: #fff; --bg-color-dark: rgba(0, 0, 0, 0.85); // Scrollbar --scrollbar-track-bg-light: rgb(220, 220, 220); --scrollbar-thumb-bg-light: var(--primary-color); --scrollbar-track-bg-dark: rgb(73, 73, 73); --scrollbar-thumb-bg-dark: var(--primary-color); // Button --btn-normal-color-light: #000000; --btn-normal-bg-light: rgb(255, 255, 255); --btn-normal-hover-color-light: var(--primary-color); --btn-normal-hover-border-color-light: var(--primary-color); --btn-normal-active-color-light: var(--secondary-color); --btn-normal-active-border-color-light: var(--secondary-color); --btn-normal-color-dark: #000000; --btn-normal-bg-dark: rgb(255, 255, 255); --btn-normal-hover-color-dark: var(--primary-color); --btn-normal-hover-border-color-dark: var(--primary-color); --btn-normal-active-color-dark: var(--secondary-color); --btn-normal-active-border-color-dark: var(--secondary-color); --btn-primary-color-light: rgb(255, 255, 255); --btn-primary-bg-light: var(--primary-color); --btn-primary-hover-bg-light: var(--secondary-color); --btn-primary-active-bg-light: var(--primary-color); --btn-primary-color-dark: rgb(255, 255, 255); --btn-primary-bg-dark: var(--primary-color); --btn-primary-hover-bg-dark: var(--secondary-color); --btn-primary-active-bg-dark: var(--primary-color); --btn-link-color-light: var(--primary-color); --btn-link-bg-light: transparent; --btn-link-hover-color-light: var(--secondary-color); --btn-link-active-color-light: var(--primary-color); --btn-link-color-dark: var(--primary-color); --btn-link-bg-dark: transparent; --btn-link-hover-color-dark: var(--secondary-color); --btn-link-active-color-dark: var(--primary-color); --btn-text-color-light: rgb(46, 46, 46); --btn-text-bg-light: transparent; --btn-text-hover-bg-light: rgb(232, 232, 232); --btn-text-active-bg-light: rgb(206, 206, 206); --btn-text-color-dark: rgb(230, 230, 230); --btn-text-bg-dark: transparent; --btn-text-hoer-color-dark: #222222; --btn-text-hover-bg-dark: rgba(255, 255, 255, 0.2); --btn-text-active-color-dark: #161616; --btn-text-active-bg-dark: rgba(255, 255, 255, 0.4); // Radio --radio-normal-color-light: #000; --radio-normal-bg-light: rgba(255, 255, 255, 1); --radio-normal-hover-color-light: var(--primary-color); --radio-primary-color-light: #fff; --radio-primary-bg-light: var(--primary-color); --radio-primary-hover-bg-light: var(--secondary-color); --radio-primary-active-bg-light: var(--primary-color); --radio-normal-color-dark: #ededed; --radio-normal-bg-dark: rgba(255, 255, 255, 0.06); --radio-normal-hover-color-dark: var(--primary-color); --radio-primary-color-dark: #fff; --radio-primary-bg-dark: var(--primary-color); --radio-primary-hover-bg-dark: var(--secondary-color); --radio-primary-active-bg-dark: var(--primary-color); // Card --card-color-light: rgb(95, 95, 95); --card-bg-light: rgba(255, 255, 255, 0.6); --card-hover-bg-light: rgba(255, 255, 255, 0.6); --card-active-bg-light: rgba(255, 255, 255, 0.4); --card-color-dark: rgb(255, 255, 255); --card-bg-dark: rgba(255, 255, 255, 0.06); --card-hover-bg-dark: rgba(255, 255, 255, 0.1); --card-active-bg-dark: rgba(255, 255, 255, 0.04); // Progress --progress-bg-light: rgba(0, 0, 0, 0.08); --progress-inner-bg-light: var(--primary-color); --progress-bg-dark: rgba(221, 221, 221, 0.08); --progress-inner-bg-dark: var(--primary-color); // Dropdown --dropdown-bg-light: rgba(255, 255, 255, 0.8); --dropdown-bg-dark: rgba(62, 62, 62, 0.8); // Modal --modal-bg-light: #f6f6f6; --modal-mask-bg-light: rgba(255, 255, 255, 0.4); --modal-bg-dark: #343434; --modal-mask-bg-dark: rgba(0, 0, 0, 0.4); // Switch --switch-on-bg-light: var(--primary-color); --switch-on-hover-bg-light: var(--secondary-color); --switch-on-dot-bg-light: #fff; --switch-on-bg-dark: var(--primary-color); --switch-on-hover-bg-dark: var(--secondary-color); --switch-on-dot-bg-dark: #fff; --switch-off-bg-light: rgba(0, 0, 0, 0.06); --switch-off-hover-bg-light: rgba(0, 0, 0, 0.1); --switch-off-dot-bg-light: #fff; --switch-off-bg-dark: rgba(255, 255, 255, 0.1); --switch-off-hover-bg-dark: rgba(255, 255, 255, 0.06); --switch-off-dot-bg-dark: #fff; // Input --input-color-light: #000; --input-bg-light: rgba(255, 255, 255, 1); --input-color-dark: #fff; --input-bg-dark: rgba(255, 255, 255, 0.06); // ColorPicker --color-picker-bg-light: #fff; --color-picker-bg-dark: rgba(255, 255, 255, 0.06); // Divider --divider-color-light: #c6c6c6; --divider-color-dark: #4d4d4d; // Select --select-bg-light: rgba(255, 255, 255, 1); --select-bg-dark: rgba(255, 255, 255, 0.06); // Toast --toast-bg-light: #fff; --toast-bg-dark: #343434; // Menu --menu-bg-light: rgba(255, 255, 255, 0.8); --menu-bg-dark: rgba(62, 62, 62, 0.8); // Table --table-tr-odd-bg-light: rgb(247, 247, 247); --table-tr-even-bg-light: rgb(238, 238, 238); --table-tr-odd-hover-bg-light: rgb(202, 202, 202); --table-tr-even-hover-bg-light: rgb(202, 202, 202); --table-tr-odd-bg-dark: #2e2e2e; --table-tr-even-bg-dark: rgb(37, 37, 37); --table-tr-odd-hover-bg-dark: rgb(61, 61, 61); --table-tr-even-hover-bg-dark: rgb(61, 61, 61); // Delay color --level-0-color: #808080; --level-1-color: #29b280; --level-2-color: #b68b1f; --level-3-color: #ea6060; --level-4-color: #f00e0e; } ================================================ FILE: frontend/src/bridge/app.ts ================================================ import * as App from '@wails/go/bridge/App' export const RestartApp = App.RestartApp export const ExitApp = App.ExitApp export const ShowMainWindow = App.ShowMainWindow export const UpdateTray = App.UpdateTray export const UpdateTrayMenus = App.UpdateTrayMenus export const UpdateTrayAndMenus = App.UpdateTrayAndMenus export const GetEnv = App.GetEnv export const IsStartup = App.IsStartup export const GetInterfaces = async () => { const { flag, data } = await App.GetInterfaces() if (!flag) { throw data } return data.split('|') } ================================================ FILE: frontend/src/bridge/exec.ts ================================================ import * as App from '@wails/go/bridge/App' import { EventsOn, EventsOff } from '@wails/runtime/runtime' import { sampleID } from '@/utils' interface ExecOptions { PidFile?: string Convert?: boolean Env?: Record StopOutputKeyword?: string WorkingDirectory?: string convert?: boolean env?: Record stopOutputKeyword?: string } const mergeExecOptions = (options: ExecOptions) => { const mergedExecOpts = { PidFile: options.PidFile ?? '', Convert: options.Convert ?? options.convert ?? false, Env: options.Env ?? options.env ?? {}, StopOutputKeyword: options.StopOutputKeyword ?? options.stopOutputKeyword ?? '', WorkingDirectory: options.WorkingDirectory ?? '', } return mergedExecOpts } export const Exec = async (path: string, args: string[], options: ExecOptions = {}) => { const { flag, data } = await App.Exec(path, args, mergeExecOptions(options)) if (!flag) { throw data } return data } export const ExecBackground = async ( path: string, args: string[] = [], onOut?: (out: string) => void, onEnd?: () => void, options: ExecOptions = {}, ) => { const outEvent = (onOut && sampleID()) || '' const endEvent = (onEnd && sampleID()) || (outEvent && sampleID()) || '' const { flag, data } = await App.ExecBackground( path, args, outEvent, endEvent, mergeExecOptions(options), ) if (!flag) { throw data } if (outEvent) { EventsOn(outEvent, onOut!) } if (endEvent) { EventsOn(endEvent, () => { outEvent && EventsOff(outEvent) EventsOff(endEvent) onEnd?.() }) } return Number(data) } export const ProcessInfo = async (pid: number) => { const { flag, data } = await App.ProcessInfo(pid) if (!flag) { throw data } return data } export const ProcessMemory = async (pid: number) => { const { flag, data } = await App.ProcessMemory(pid) if (!flag) { throw data } return Number(data) } export const KillProcess = async (pid: number, timeout = 10) => { const { flag, data } = await App.KillProcess(pid, timeout) if (!flag) { throw data } return data } ================================================ FILE: frontend/src/bridge/index.ts ================================================ export * from '@wails/runtime/runtime' export * from './io' export * from './net' export * from './exec' export * from './app' export * from './server' export * from './mmdb' export * from './notification' ================================================ FILE: frontend/src/bridge/io.ts ================================================ import * as App from '@wails/go/bridge/App' interface IOOptions { Mode?: 'Binary' | 'Text' Range?: string } export const WriteFile = async (path: string, content: string, options: IOOptions = {}) => { const { flag, data } = await App.WriteFile(path, content, { Mode: 'Text', Range: '', ...options }) if (!flag) { throw data } return data } export const ReadFile = async (path: string, options: IOOptions = {}) => { const { flag, data } = await App.ReadFile(path, { Mode: 'Text', Range: '', ...options }) if (!flag) { throw data } return data } export const MoveFile = async (source: string, target: string) => { const { flag, data } = await App.MoveFile(source, target) if (!flag) { throw data } return data } export const RemoveFile = async (path: string) => { const { flag, data } = await App.RemoveFile(path) if (!flag) { throw data } return data } export const CopyFile = async (source: string, target: string) => { const { flag, data } = await App.CopyFile(source, target) if (!flag) { throw data } return data } export const FileExists = async (path: string) => { const { flag, data } = await App.FileExists(path) if (!flag) { throw data } return data === 'true' } export const AbsolutePath = async (path: string) => { const { flag, data } = await App.AbsolutePath(path) if (!flag) { throw data } return data } export const MakeDir = async (path: string) => { const { flag, data } = await App.MakeDir(path) if (!flag) { throw data } return data } export const ReadDir = async (path: string) => { const { flag, data } = await App.ReadDir(path) if (!flag) { throw data } return data .split('|') .filter((v) => v) .map((v) => { const [name, size, isDir] = v.split(',') as [string, string, string] return { name, size: Number(size), isDir: isDir === 'true' } }) } export const OpenDir = async (path: string) => { const { flag, data } = await App.OpenDir(path) if (!flag) { throw data } return data } export const OpenURI = async (uri: string) => { const { flag, data } = await App.OpenURI(uri) if (!flag) { throw data } return data } export const UnzipZIPFile = async (path: string, output: string) => { const { flag, data } = await App.UnzipZIPFile(path, output) if (!flag) { throw data } return data } export const UnzipGZFile = async (path: string, output: string) => { const { flag, data } = await App.UnzipGZFile(path, output) if (!flag) { throw data } return data } export const UnzipTarGZFile = async (path: string, output: string) => { const { flag, data } = await App.UnzipTarGZFile(path, output) if (!flag) { throw data } return data } ================================================ FILE: frontend/src/bridge/mmdb.ts ================================================ import * as App from '@wails/go/bridge/App' type QueryType = | 'ASN' | 'AnonymousIP' | 'City' | 'ConnectionType' | 'Country' | 'Domain' | 'Enterprise' export const OpenMMDB = async (path: string, id: string) => { const { flag, data } = await App.OpenMMDB(path, id) if (!flag) { throw data } return { close: () => CloseMMDB(path, id), query: (ip: string, type: QueryType) => QueryMMDB(path, ip, type), } } export const CloseMMDB = async (path: string, id: string) => { const { flag, data } = await App.CloseMMDB(path, id) if (!flag) { throw data } return data } export const QueryMMDB = async (path: string, ip: string, type: QueryType = 'Country') => { const { flag, data } = await App.QueryMMDB(path, ip, type) if (!flag) { throw data } return JSON.parse(data) } ================================================ FILE: frontend/src/bridge/net.ts ================================================ import * as App from '@wails/go/bridge/App' import { EventsOn, EventsOff, EventsEmit } from '@wails/runtime/runtime' import { RequestMethod } from '@/enums/app' import { sampleID, getUserAgent } from '@/utils' import { GetSystemOrKernelProxy } from '@/utils/helper' interface Request { method: RequestMethod url: string headers?: { 'Content-Type'?: 'application/json' | 'application/x-www-form-urlencoded' | 'text/plain' } & Record body?: any options?: { Proxy?: string Insecure?: boolean Redirect?: boolean Timeout?: number CancelId?: string FileField?: string } } interface Response { status: number headers: Record body: T } const mergeRequestOptions = async (options: Request['options']) => { const mergedReqOpts: Required = { Proxy: await GetSystemOrKernelProxy(), Insecure: false, Redirect: true, Timeout: 15, // 15 seconds CancelId: '', FileField: 'file', ...options, } return mergedReqOpts } const transformResponseHeaders = (headers: Record): Response['headers'] => { return Object.fromEntries( Object.entries(headers).map(([key, value]) => [key, value.length > 1 ? value : value[0]!]), ) } const transformResponseBody = (body: Response['body'], headers: Response['headers']) => { if (headers['Content-Type']?.includes('application/json')) { try { body = JSON.parse(body) } catch { console.warn('Failed to parse response body as JSON:', body) } } return body as T } const transformRequest = async ( headers: Request['headers'], body: Request['body'], options: Request['options'], ) => { const transformedHeaders = { 'User-Agent': getUserAgent(), ...headers } if (transformedHeaders['Content-Type']?.includes('application/json')) { body && (body = JSON.stringify(body)) } else if (transformedHeaders['Content-Type']?.includes('application/x-www-form-urlencoded')) { body && (body = new URLSearchParams(body).toString()) } const transformedReqOpts = await mergeRequestOptions(options) return [transformedHeaders, body, transformedReqOpts] as const } const transformResponse = ( status: Response['status'], headers: Record, body: Response['body'], ) => { const transformedHeaders = transformResponseHeaders(headers) const transformedBody = transformResponseBody(body, transformedHeaders) return { status, headers: transformedHeaders, body: transformedBody } } interface RequestWithProgressOptions { Method?: Request['method'] } const requestWithProgress = (fnName: 'Download' | 'Upload') => { return async ( url: Request['url'], path: string, headers: Request['headers'] = {}, progress?: (progress: number, total: number) => void, options: Request['options'] & RequestWithProgressOptions = {}, ) => { const [_headers, , _options] = await transformRequest(headers, null, { Timeout: 20 * 60, // 20 minutes ...options, }) const method = options.Method ?? { Download: RequestMethod.Get, Upload: RequestMethod.Post }[fnName] const progressEvent = (progress && sampleID()) || '' if (progressEvent) { EventsOn(progressEvent, progress!) } const { flag, status, headers: respHeaders, body: respBody, } = await App[fnName](method, url, path, _headers, progressEvent, _options) if (progressEvent) { EventsOff(progressEvent) } if (!flag) throw respBody return transformResponse(status, respHeaders, respBody) } } const requestWithBody = (method: RequestMethod.Put | RequestMethod.Post | RequestMethod.Patch) => { return async ( url: string, headers: Request['headers'] = {}, body = {}, options = {}, ) => { const [_headers, _body, _options] = await transformRequest(headers, body, options) const { flag, status, headers: respHeaders, body: respBody, } = await App.Requests(method, url, _headers, _body, _options) if (!flag) throw respBody return transformResponse(status, respHeaders, respBody) } } const requestWithoutBody = ( methd: RequestMethod.Get | RequestMethod.Head | RequestMethod.Delete, ) => { return async ( url: string, headers: Request['headers'] = {}, options: Request['options'] = {}, ) => { const [_headers, , _options] = await transformRequest(headers, null, options) const { flag, status, headers: respHeaders, body, } = await App.Requests(methd, url, _headers, '', _options) if (!flag) throw body return transformResponse(status, respHeaders, body) } } interface RequestWithAutoTransform extends Request { autoTransformBody?: boolean } export const Requests = async (options: RequestWithAutoTransform) => { const { method = 'GET', url, headers = {}, body = '', options: reqOpts = {} } = options const [reqHeaders, reqBody, finalReqOpts] = await transformRequest(headers, body, reqOpts) const { flag, status, headers: respHeaders, body: respBody, } = await App.Requests(method.toUpperCase(), url, reqHeaders, reqBody, finalReqOpts) if (!flag) throw respBody const transformedHeaders = transformResponseHeaders(respHeaders) const transformBody = options.autoTransformBody ?? true return { status, headers: transformedHeaders, body: transformBody ? transformResponseBody(respBody, transformedHeaders) : (respBody as T), } } export const Upload = requestWithProgress('Upload') export const Download = requestWithProgress('Download') export const HttpGet = requestWithoutBody(RequestMethod.Get) export const HttpHead = requestWithoutBody(RequestMethod.Head) export const HttpDelete = requestWithoutBody(RequestMethod.Delete) export const HttpPut = requestWithBody(RequestMethod.Put) export const HttpPost = requestWithBody(RequestMethod.Post) export const HttpPatch = requestWithBody(RequestMethod.Patch) export const HttpCancel = (cancelId: string) => EventsEmit(cancelId) ================================================ FILE: frontend/src/bridge/notification.ts ================================================ import * as App from '@wails/go/bridge/App' import { APP_TITLE } from '@/utils' interface NotifyOptions { AppName?: string Beep?: boolean } export const Notify = async ( title: string, message: string, icon = '', options: NotifyOptions = {}, ) => { const _options: Required = { AppName: APP_TITLE, Beep: true, ...options } const icons: Record = { success: 'data/.cache/imgs/notify_success.png', error: 'data/.cache/imgs/notify_error.png', } const { flag, data } = await App.Notify( title, message, icons[icon] || 'data/.cache/imgs/tray_normal_dark.png', _options, ) if (!flag) { throw data } return data } ================================================ FILE: frontend/src/bridge/server.ts ================================================ import * as App from '@wails/go/bridge/App' import { EventsOn, EventsEmit, EventsOff } from '@wails/runtime/runtime' interface Request { id: string method: string url: string headers: Record body: string } interface Response { status: number headers: Record body: string options: { mode: 'Binary' | 'Text' } } interface ServerOptions { Cert?: string Key?: string StaticPath?: string StaticRoute?: string StaticHeaders?: Recordable UploadPath?: string UploadRoute?: string UploadHeaders?: Recordable MaxUploadSize?: number } type HttpServerHandler = ( req: Request, res: { end: ( status: Response['status'], headers: Response['headers'], body: Response['body'], options: Response['options'], ) => void }, ) => Promise export const StartServer = async ( address: string, id: string, handler: HttpServerHandler, options: ServerOptions = {}, ) => { const _options: Required = { Cert: '', Key: '', StaticPath: '', // default: /static StaticRoute: '/static/', StaticHeaders: {}, UploadPath: '', // default: /upload UploadRoute: '/upload', UploadHeaders: {}, MaxUploadSize: 50 * 1024 * 1024, // 50MB ...options, } const { flag, data } = await App.StartServer(address, id, _options) if (!flag) { throw data } EventsOn(id, async (...args) => { const [id, method, url, headers, body] = args try { await handler( { id, method, url, headers: Object.entries(headers).reduce((p, c: any) => ({ ...p, [c[0]]: c[1][0] }), {}), body, }, { end: (status, headers, body, options = { mode: 'Text' }) => { EventsEmit(id, status, JSON.stringify(headers), body, JSON.stringify(options)) }, }, ) } catch (err: any) { console.log('Server handler err:', err, id) EventsEmit( id, 500, JSON.stringify({ 'Content-Type': 'text/plain; charset=utf-8' }), err.message || err, JSON.stringify({ Mode: 'Text' }), ) } }) return { close: () => StopServer(id) } } export const StopServer = async (serverID: string) => { const { flag, data } = await App.StopServer(serverID) if (!flag) { throw data } EventsOff(serverID) return data } export const ListServer = async () => { const { flag, data } = await App.ListServer() if (!flag) { throw data } return data.split('|').filter((id) => id.length) } ================================================ FILE: frontend/src/bridge/wailsjs/go/bridge/App.d.ts ================================================ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {bridge} from '../models'; export function AbsolutePath(arg1:string):Promise; export function CloseMMDB(arg1:string,arg2:string):Promise; export function CopyFile(arg1:string,arg2:string):Promise; export function Download(arg1:string,arg2:string,arg3:string,arg4:Record,arg5:string,arg6:bridge.RequestOptions):Promise; export function Exec(arg1:string,arg2:Array,arg3:bridge.ExecOptions):Promise; export function ExecBackground(arg1:string,arg2:Array,arg3:string,arg4:string,arg5:bridge.ExecOptions):Promise; export function ExitApp():Promise; export function FileExists(arg1:string):Promise; export function GetEnv():Promise; export function GetInterfaces():Promise; export function IsStartup():Promise; export function KillProcess(arg1:number,arg2:number):Promise; export function ListServer():Promise; export function MakeDir(arg1:string):Promise; export function MoveFile(arg1:string,arg2:string):Promise; export function Notify(arg1:string,arg2:string,arg3:string,arg4:bridge.NotifyOptions):Promise; export function OpenDir(arg1:string):Promise; export function OpenMMDB(arg1:string,arg2:string):Promise; export function OpenURI(arg1:string):Promise; export function ProcessInfo(arg1:number):Promise; export function ProcessMemory(arg1:number):Promise; export function QueryMMDB(arg1:string,arg2:string,arg3:string):Promise; export function ReadDir(arg1:string):Promise; export function ReadFile(arg1:string,arg2:bridge.IOOptions):Promise; export function RemoveFile(arg1:string):Promise; export function Requests(arg1:string,arg2:string,arg3:Record,arg4:string,arg5:bridge.RequestOptions):Promise; export function RestartApp():Promise; export function ShowMainWindow():Promise; export function StartServer(arg1:string,arg2:string,arg3:bridge.ServerOptions):Promise; export function StopServer(arg1:string):Promise; export function UnzipGZFile(arg1:string,arg2:string):Promise; export function UnzipTarGZFile(arg1:string,arg2:string):Promise; export function UnzipZIPFile(arg1:string,arg2:string):Promise; export function UpdateTray(arg1:bridge.TrayContent):Promise; export function UpdateTrayAndMenus(arg1:bridge.TrayContent,arg2:Array):Promise; export function UpdateTrayMenus(arg1:Array):Promise; export function Upload(arg1:string,arg2:string,arg3:string,arg4:Record,arg5:string,arg6:bridge.RequestOptions):Promise; export function WriteFile(arg1:string,arg2:string,arg3:bridge.IOOptions):Promise; ================================================ FILE: frontend/src/bridge/wailsjs/go/bridge/App.js ================================================ // @ts-check // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT export function AbsolutePath(arg1) { return window['go']['bridge']['App']['AbsolutePath'](arg1); } export function CloseMMDB(arg1, arg2) { return window['go']['bridge']['App']['CloseMMDB'](arg1, arg2); } export function CopyFile(arg1, arg2) { return window['go']['bridge']['App']['CopyFile'](arg1, arg2); } export function Download(arg1, arg2, arg3, arg4, arg5, arg6) { return window['go']['bridge']['App']['Download'](arg1, arg2, arg3, arg4, arg5, arg6); } export function Exec(arg1, arg2, arg3) { return window['go']['bridge']['App']['Exec'](arg1, arg2, arg3); } export function ExecBackground(arg1, arg2, arg3, arg4, arg5) { return window['go']['bridge']['App']['ExecBackground'](arg1, arg2, arg3, arg4, arg5); } export function ExitApp() { return window['go']['bridge']['App']['ExitApp'](); } export function FileExists(arg1) { return window['go']['bridge']['App']['FileExists'](arg1); } export function GetEnv() { return window['go']['bridge']['App']['GetEnv'](); } export function GetInterfaces() { return window['go']['bridge']['App']['GetInterfaces'](); } export function IsStartup() { return window['go']['bridge']['App']['IsStartup'](); } export function KillProcess(arg1, arg2) { return window['go']['bridge']['App']['KillProcess'](arg1, arg2); } export function ListServer() { return window['go']['bridge']['App']['ListServer'](); } export function MakeDir(arg1) { return window['go']['bridge']['App']['MakeDir'](arg1); } export function MoveFile(arg1, arg2) { return window['go']['bridge']['App']['MoveFile'](arg1, arg2); } export function Notify(arg1, arg2, arg3, arg4) { return window['go']['bridge']['App']['Notify'](arg1, arg2, arg3, arg4); } export function OpenDir(arg1) { return window['go']['bridge']['App']['OpenDir'](arg1); } export function OpenMMDB(arg1, arg2) { return window['go']['bridge']['App']['OpenMMDB'](arg1, arg2); } export function OpenURI(arg1) { return window['go']['bridge']['App']['OpenURI'](arg1); } export function ProcessInfo(arg1) { return window['go']['bridge']['App']['ProcessInfo'](arg1); } export function ProcessMemory(arg1) { return window['go']['bridge']['App']['ProcessMemory'](arg1); } export function QueryMMDB(arg1, arg2, arg3) { return window['go']['bridge']['App']['QueryMMDB'](arg1, arg2, arg3); } export function ReadDir(arg1) { return window['go']['bridge']['App']['ReadDir'](arg1); } export function ReadFile(arg1, arg2) { return window['go']['bridge']['App']['ReadFile'](arg1, arg2); } export function RemoveFile(arg1) { return window['go']['bridge']['App']['RemoveFile'](arg1); } export function Requests(arg1, arg2, arg3, arg4, arg5) { return window['go']['bridge']['App']['Requests'](arg1, arg2, arg3, arg4, arg5); } export function RestartApp() { return window['go']['bridge']['App']['RestartApp'](); } export function ShowMainWindow() { return window['go']['bridge']['App']['ShowMainWindow'](); } export function StartServer(arg1, arg2, arg3) { return window['go']['bridge']['App']['StartServer'](arg1, arg2, arg3); } export function StopServer(arg1) { return window['go']['bridge']['App']['StopServer'](arg1); } export function UnzipGZFile(arg1, arg2) { return window['go']['bridge']['App']['UnzipGZFile'](arg1, arg2); } export function UnzipTarGZFile(arg1, arg2) { return window['go']['bridge']['App']['UnzipTarGZFile'](arg1, arg2); } export function UnzipZIPFile(arg1, arg2) { return window['go']['bridge']['App']['UnzipZIPFile'](arg1, arg2); } export function UpdateTray(arg1) { return window['go']['bridge']['App']['UpdateTray'](arg1); } export function UpdateTrayAndMenus(arg1, arg2) { return window['go']['bridge']['App']['UpdateTrayAndMenus'](arg1, arg2); } export function UpdateTrayMenus(arg1) { return window['go']['bridge']['App']['UpdateTrayMenus'](arg1); } export function Upload(arg1, arg2, arg3, arg4, arg5, arg6) { return window['go']['bridge']['App']['Upload'](arg1, arg2, arg3, arg4, arg5, arg6); } export function WriteFile(arg1, arg2, arg3) { return window['go']['bridge']['App']['WriteFile'](arg1, arg2, arg3); } ================================================ FILE: frontend/src/bridge/wailsjs/go/models.ts ================================================ export namespace bridge { export class EnvResult { appName: string; appVersion: string; basePath: string; os: string; arch: string; isPrivileged: boolean; static createFrom(source: any = {}) { return new EnvResult(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.appName = source["appName"]; this.appVersion = source["appVersion"]; this.basePath = source["basePath"]; this.os = source["os"]; this.arch = source["arch"]; this.isPrivileged = source["isPrivileged"]; } } export class ExecOptions { PidFile: string; StopOutputKeyword: string; WorkingDirectory: string; Convert: boolean; Env: Record; static createFrom(source: any = {}) { return new ExecOptions(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.PidFile = source["PidFile"]; this.StopOutputKeyword = source["StopOutputKeyword"]; this.WorkingDirectory = source["WorkingDirectory"]; this.Convert = source["Convert"]; this.Env = source["Env"]; } } export class FlagResult { flag: boolean; data: string; static createFrom(source: any = {}) { return new FlagResult(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.flag = source["flag"]; this.data = source["data"]; } } export class HTTPResult { flag: boolean; status: number; headers: Record>; body: string; static createFrom(source: any = {}) { return new HTTPResult(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.flag = source["flag"]; this.status = source["status"]; this.headers = source["headers"]; this.body = source["body"]; } } export class IOOptions { Mode: string; Range: string; static createFrom(source: any = {}) { return new IOOptions(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.Mode = source["Mode"]; this.Range = source["Range"]; } } export class MenuItem { type: string; text: string; tooltip: string; event: string; children: MenuItem[]; hidden: boolean; checked: boolean; static createFrom(source: any = {}) { return new MenuItem(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.type = source["type"]; this.text = source["text"]; this.tooltip = source["tooltip"]; this.event = source["event"]; this.children = this.convertValues(source["children"], MenuItem); this.hidden = source["hidden"]; this.checked = source["checked"]; } convertValues(a: any, classs: any, asMap: boolean = false): any { if (!a) { return a; } if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { for (const key of Object.keys(a)) { a[key] = new classs(a[key]); } return a; } return new classs(a); } return a; } } export class NotifyOptions { AppName: string; Beep: boolean; static createFrom(source: any = {}) { return new NotifyOptions(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.AppName = source["AppName"]; this.Beep = source["Beep"]; } } export class RequestOptions { Proxy: string; Insecure: boolean; Redirect: boolean; Timeout: number; CancelId: string; FileField: string; static createFrom(source: any = {}) { return new RequestOptions(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.Proxy = source["Proxy"]; this.Insecure = source["Insecure"]; this.Redirect = source["Redirect"]; this.Timeout = source["Timeout"]; this.CancelId = source["CancelId"]; this.FileField = source["FileField"]; } } export class ServerOptions { Cert: string; Key: string; StaticPath: string; StaticRoute: string; StaticHeaders: Record; UploadPath: string; UploadRoute: string; UploadHeaders: Record; MaxUploadSize: number; static createFrom(source: any = {}) { return new ServerOptions(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.Cert = source["Cert"]; this.Key = source["Key"]; this.StaticPath = source["StaticPath"]; this.StaticRoute = source["StaticRoute"]; this.StaticHeaders = source["StaticHeaders"]; this.UploadPath = source["UploadPath"]; this.UploadRoute = source["UploadRoute"]; this.UploadHeaders = source["UploadHeaders"]; this.MaxUploadSize = source["MaxUploadSize"]; } } export class TrayContent { icon?: string; title?: string; tooltip?: string; static createFrom(source: any = {}) { return new TrayContent(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.icon = source["icon"]; this.title = source["title"]; this.tooltip = source["tooltip"]; } } } ================================================ FILE: frontend/src/bridge/wailsjs/runtime/package.json ================================================ { "name": "@wailsapp/runtime", "version": "2.0.0", "description": "Wails Javascript runtime library", "main": "runtime.js", "types": "runtime.d.ts", "scripts": { }, "repository": { "type": "git", "url": "git+https://github.com/wailsapp/wails.git" }, "keywords": [ "Wails", "Javascript", "Go" ], "author": "Lea Anthony ", "license": "MIT", "bugs": { "url": "https://github.com/wailsapp/wails/issues" }, "homepage": "https://github.com/wailsapp/wails#readme" } ================================================ FILE: frontend/src/bridge/wailsjs/runtime/runtime.d.ts ================================================ /* _ __ _ __ | | / /___ _(_) /____ | | /| / / __ `/ / / ___/ | |/ |/ / /_/ / / (__ ) |__/|__/\__,_/_/_/____/ The electron alternative for Go (c) Lea Anthony 2019-present */ export interface Position { x: number; y: number; } export interface Size { w: number; h: number; } export interface Screen { isCurrent: boolean; isPrimary: boolean; width : number height : number } // Environment information such as platform, buildtype, ... export interface EnvironmentInfo { buildType: string; platform: string; arch: string; } // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) // emits the given event. Optional data may be passed with the event. // This will trigger any event listeners. export function EventsEmit(eventName: string, ...data: any): void; // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) // sets up a listener for the given event name, but will only trigger a given number times. export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) // sets up a listener for the given event name, but will only trigger once. export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) // unregisters the listener for the given event name. export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) // unregisters all listeners. export function EventsOffAll(): void; // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) // logs the given message at the `trace` log level. export function LogTrace(message: string): void; // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) // logs the given message at the `debug` log level. export function LogDebug(message: string): void; // [LogError](https://wails.io/docs/reference/runtime/log#logerror) // logs the given message at the `error` log level. export function LogError(message: string): void; // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) // logs the given message at the `fatal` log level. // The application will quit after calling this method. export function LogFatal(message: string): void; // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) // logs the given message at the `info` log level. export function LogInfo(message: string): void; // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) // logs the given message at the `warning` log level. export function LogWarning(message: string): void; // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) // Forces a reload by the main application as well as connected browsers. export function WindowReload(): void; // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) // Reloads the application frontend. export function WindowReloadApp(): void; // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) // Sets the window AlwaysOnTop or not on top. export function WindowSetAlwaysOnTop(b: boolean): void; // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) // *Windows only* // Sets window theme to system default (dark/light). export function WindowSetSystemDefaultTheme(): void; // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) // *Windows only* // Sets window to light theme. export function WindowSetLightTheme(): void; // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) // *Windows only* // Sets window to dark theme. export function WindowSetDarkTheme(): void; // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) // Centers the window on the monitor the window is currently on. export function WindowCenter(): void; // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) // Sets the text in the window title bar. export function WindowSetTitle(title: string): void; // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) // Makes the window full screen. export function WindowFullscreen(): void; // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) // Restores the previous window dimensions and position prior to full screen. export function WindowUnfullscreen(): void; // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) // Returns the state of the window, i.e. whether the window is in full screen mode or not. export function WindowIsFullscreen(): Promise; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. export function WindowGetSize(): Promise; // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. // Setting a size of 0,0 will disable this constraint. export function WindowSetMaxSize(width: number, height: number): void; // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. // Setting a size of 0,0 will disable this constraint. export function WindowSetMinSize(width: number, height: number): void; // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) // Sets the window position relative to the monitor the window is currently on. export function WindowSetPosition(x: number, y: number): void; // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) // Gets the window position relative to the monitor the window is currently on. export function WindowGetPosition(): Promise; // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) // Hides the window. export function WindowHide(): void; // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) // Shows the window, if it is currently hidden. export function WindowShow(): void; // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) // Maximises the window to fill the screen. export function WindowMaximise(): void; // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) // Toggles between Maximised and UnMaximised. export function WindowToggleMaximise(): void; // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) // Restores the window to the dimensions and position prior to maximising. export function WindowUnmaximise(): void; // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) // Returns the state of the window, i.e. whether the window is maximised or not. export function WindowIsMaximised(): Promise; // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) // Minimises the window. export function WindowMinimise(): void; // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) // Restores the window to the dimensions and position prior to minimising. export function WindowUnminimise(): void; // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) // Returns the state of the window, i.e. whether the window is minimised or not. export function WindowIsMinimised(): Promise; // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) // Returns the state of the window, i.e. whether the window is normal or not. export function WindowIsNormal(): Promise; // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. export function ScreenGetAll(): Promise; // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) // Opens the given URL in the system browser. export function BrowserOpenURL(url: string): void; // [Environment](https://wails.io/docs/reference/runtime/intro#environment) // Returns information about the environment export function Environment(): Promise; // [Quit](https://wails.io/docs/reference/runtime/intro#quit) // Quits the application. export function Quit(): void; // [Hide](https://wails.io/docs/reference/runtime/intro#hide) // Hides the application. export function Hide(): void; // [Show](https://wails.io/docs/reference/runtime/intro#show) // Shows the application. export function Show(): void; // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) // Returns the current text stored on clipboard export function ClipboardGetText(): Promise; // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) // Sets a text on the clipboard export function ClipboardSetText(text: string): Promise; // [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) // OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void // [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) // OnFileDropOff removes the drag and drop listeners and handlers. export function OnFileDropOff() :void // Check if the file path resolver is available export function CanResolveFilePaths(): boolean; // Resolves file paths for an array of files export function ResolveFilePaths(files: File[]): void ================================================ FILE: frontend/src/bridge/wailsjs/runtime/runtime.js ================================================ /* _ __ _ __ | | / /___ _(_) /____ | | /| / / __ `/ / / ___/ | |/ |/ / /_/ / / (__ ) |__/|__/\__,_/_/_/____/ The electron alternative for Go (c) Lea Anthony 2019-present */ export function LogPrint(message) { window.runtime.LogPrint(message); } export function LogTrace(message) { window.runtime.LogTrace(message); } export function LogDebug(message) { window.runtime.LogDebug(message); } export function LogInfo(message) { window.runtime.LogInfo(message); } export function LogWarning(message) { window.runtime.LogWarning(message); } export function LogError(message) { window.runtime.LogError(message); } export function LogFatal(message) { window.runtime.LogFatal(message); } export function EventsOnMultiple(eventName, callback, maxCallbacks) { return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); } export function EventsOn(eventName, callback) { return EventsOnMultiple(eventName, callback, -1); } export function EventsOff(eventName, ...additionalEventNames) { return window.runtime.EventsOff(eventName, ...additionalEventNames); } export function EventsOffAll() { return window.runtime.EventsOffAll(); } export function EventsOnce(eventName, callback) { return EventsOnMultiple(eventName, callback, 1); } export function EventsEmit(eventName) { let args = [eventName].slice.call(arguments); return window.runtime.EventsEmit.apply(null, args); } export function WindowReload() { window.runtime.WindowReload(); } export function WindowReloadApp() { window.runtime.WindowReloadApp(); } export function WindowSetAlwaysOnTop(b) { window.runtime.WindowSetAlwaysOnTop(b); } export function WindowSetSystemDefaultTheme() { window.runtime.WindowSetSystemDefaultTheme(); } export function WindowSetLightTheme() { window.runtime.WindowSetLightTheme(); } export function WindowSetDarkTheme() { window.runtime.WindowSetDarkTheme(); } export function WindowCenter() { window.runtime.WindowCenter(); } export function WindowSetTitle(title) { window.runtime.WindowSetTitle(title); } export function WindowFullscreen() { window.runtime.WindowFullscreen(); } export function WindowUnfullscreen() { window.runtime.WindowUnfullscreen(); } export function WindowIsFullscreen() { return window.runtime.WindowIsFullscreen(); } export function WindowGetSize() { return window.runtime.WindowGetSize(); } export function WindowSetSize(width, height) { window.runtime.WindowSetSize(width, height); } export function WindowSetMaxSize(width, height) { window.runtime.WindowSetMaxSize(width, height); } export function WindowSetMinSize(width, height) { window.runtime.WindowSetMinSize(width, height); } export function WindowSetPosition(x, y) { window.runtime.WindowSetPosition(x, y); } export function WindowGetPosition() { return window.runtime.WindowGetPosition(); } export function WindowHide() { window.runtime.WindowHide(); } export function WindowShow() { window.runtime.WindowShow(); } export function WindowMaximise() { window.runtime.WindowMaximise(); } export function WindowToggleMaximise() { window.runtime.WindowToggleMaximise(); } export function WindowUnmaximise() { window.runtime.WindowUnmaximise(); } export function WindowIsMaximised() { return window.runtime.WindowIsMaximised(); } export function WindowMinimise() { window.runtime.WindowMinimise(); } export function WindowUnminimise() { window.runtime.WindowUnminimise(); } export function WindowSetBackgroundColour(R, G, B, A) { window.runtime.WindowSetBackgroundColour(R, G, B, A); } export function ScreenGetAll() { return window.runtime.ScreenGetAll(); } export function WindowIsMinimised() { return window.runtime.WindowIsMinimised(); } export function WindowIsNormal() { return window.runtime.WindowIsNormal(); } export function BrowserOpenURL(url) { window.runtime.BrowserOpenURL(url); } export function Environment() { return window.runtime.Environment(); } export function Quit() { window.runtime.Quit(); } export function Hide() { window.runtime.Hide(); } export function Show() { window.runtime.Show(); } export function ClipboardGetText() { return window.runtime.ClipboardGetText(); } export function ClipboardSetText(text) { return window.runtime.ClipboardSetText(text); } /** * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. * * @export * @callback OnFileDropCallback * @param {number} x - x coordinate of the drop * @param {number} y - y coordinate of the drop * @param {string[]} paths - A list of file paths. */ /** * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. * * @export * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) */ export function OnFileDrop(callback, useDropTarget) { return window.runtime.OnFileDrop(callback, useDropTarget); } /** * OnFileDropOff removes the drag and drop listeners and handlers. */ export function OnFileDropOff() { return window.runtime.OnFileDropOff(); } export function CanResolveFilePaths() { return window.runtime.CanResolveFilePaths(); } export function ResolveFilePaths(files) { return window.runtime.ResolveFilePaths(files); } ================================================ FILE: frontend/src/components/Button/index.vue ================================================ ================================================ FILE: frontend/src/components/Card/index.vue ================================================ ================================================ FILE: frontend/src/components/CheckBox/index.vue ================================================ ================================================ FILE: frontend/src/components/CodeViewer/index.vue ================================================ ================================================ FILE: frontend/src/components/ColorPicker/index.vue ================================================ ================================================ FILE: frontend/src/components/Confirm/index.vue ================================================ ================================================ FILE: frontend/src/components/CustomAction/index.vue ================================================ ================================================ FILE: frontend/src/components/Divider/index.vue ================================================ ================================================ FILE: frontend/src/components/Dropdown/index.vue ================================================ ================================================ FILE: frontend/src/components/Empty/index.vue ================================================ ================================================ FILE: frontend/src/components/Icon/icons.ts ================================================ export const icons = { messageSuccess: ``, messageError: ``, messageInfo: ``, messageWarn: ``, clear: ``, overview: ``, profiles: ``, subscriptions: ``, rulesets: ``, plugins: ``, scheduledTasks: ``, settings: ``, arrowDown: ``, arrowLeft: ``, arrowRight: ``, clear3: ``, disabled: ``, empty: ``, reset: ``, telegram: ``, copy: ``, paste: ``, link: ``, loading: ``, log: ``, minimize: ``, maximize: ``, maximize2: ``, more: ``, pinFill: ``, pin: ``, preview: ``, refresh: ``, restartApp: ``, restart: ``, rollback: ``, selected: ``, settings2: ``, settings3: ``, sparkle: ``, speedTest: ``, stop: ``, add: ``, clear2: ``, close: ``, code: ``, collapse: ``, delete: ``, drag: ``, edit: ``, error: ``, expand: ``, file: ``, filter: ``, folder: ``, forbidden: ``, backward: ``, pause: ``, play: ``, forward: ``, github: ``, grant: ``, } export type IconName = keyof typeof icons ================================================ FILE: frontend/src/components/Icon/index.vue ================================================ ================================================ FILE: frontend/src/components/Input/index.vue ================================================ ================================================ FILE: frontend/src/components/InputList/index.vue ================================================ ================================================ FILE: frontend/src/components/InterfaceSelect/index.vue ================================================ ================================================ FILE: frontend/src/components/KeyValueEditor/index.vue ================================================ ================================================ FILE: frontend/src/components/Menu/index.vue ================================================ ================================================ FILE: frontend/src/components/Message/index.vue ================================================ ================================================ FILE: frontend/src/components/Modal/index.ts ================================================ import { ref, defineComponent, h, computed, type VNode, type ComponentPublicInstance } from 'vue' import Modal from './index.vue' import type { Props as ModalProps, Slots as ModalSlots } from './index.vue' export const useModal = (options: Partial, contents: ModalSlots = {}) => { const open = ref(false) const props = ref(options) const slots = ref(contents) if ('component' in options && options.component) { console.warn( '[Deprecated] The "component" option is deprecated. Please use the second parameter instead, e.g. \n{ \n\tdefault: () => any \n}', ) slots.value.default = () => options.component } const modal = defineComponent({ setup(_, ctx) { const mergedProps = computed(() => ({ ...props.value, ...ctx.attrs, open: open.value, 'onUpdate:open': (val: boolean) => (open.value = val), })) return () => h(Modal, mergedProps.value, { ...slots.value, ...ctx.slots }) }, }) const api = { open: () => (open.value = true), close: () => (open.value = false), setProps(options: Partial & Recordable) { props.value = options return this }, patchProps(options: Partial & Recordable) { Object.assign(props.value, options) return this }, setSlots(_slots: ModalSlots) { slots.value = _slots return this }, patchSlots(_slots: ModalSlots) { Object.assign(slots.value, _slots) return this }, setContent any>( Comp: C, _props?: InstanceType['$props'], _slots?: InstanceType['$slots'], replace = true, ) { const defaultSlot = () => h( Comp, { ..._props, ref: (el: ComponentPublicInstance<{ modalSlots: ModalSlots }> | null) => { if (el?.modalSlots) this.patchSlots(el?.modalSlots || {}) }, }, _slots, ) if (replace) { slots.value = { default: defaultSlot } } else { slots.value.default = defaultSlot } return this }, // Compatibility code setComponent(comp: VNode) { console.warn('[Deprecated] "setComponent" is deprecated. Please use "setContent" instead.') slots.value.default = () => comp return this }, } return [modal, api] as const } ================================================ FILE: frontend/src/components/Modal/index.vue ================================================ ================================================ FILE: frontend/src/components/MultipleSelect/index.vue ================================================ ================================================ FILE: frontend/src/components/Pagination/index.vue ================================================ ================================================ FILE: frontend/src/components/Picker/index.vue ================================================ ================================================ FILE: frontend/src/components/Progress/index.vue ================================================ ================================================ FILE: frontend/src/components/Prompt/index.vue ================================================ ================================================ FILE: frontend/src/components/Radio/index.vue ================================================ ================================================ FILE: frontend/src/components/Select/index.vue ================================================ ================================================ FILE: frontend/src/components/Switch/index.vue ================================================ ================================================ FILE: frontend/src/components/Table/index.vue ================================================ ================================================ FILE: frontend/src/components/Tabs/index.vue ================================================ ================================================ FILE: frontend/src/components/Tag/index.vue ================================================ ================================================ FILE: frontend/src/components/Tips/index.vue ================================================ ================================================ FILE: frontend/src/components/TrafficChart/index.vue ================================================ ================================================ FILE: frontend/src/components/_common/AboutView.vue ================================================ ================================================ FILE: frontend/src/components/_common/CommandView.vue ================================================ ================================================ FILE: frontend/src/components/_common/NavigationBar.vue ================================================ ================================================ FILE: frontend/src/components/_common/SplashView.vue ================================================ ================================================ FILE: frontend/src/components/_common/TitleBar.vue ================================================ ================================================ FILE: frontend/src/components/components.d.ts ================================================ export {} declare module 'vue' { export interface GlobalComponents { Button: (typeof import('./Button/index.vue'))['default'] Card: (typeof import('./Card/index.vue'))['default'] CheckBox: (typeof import('./CheckBox/index.vue'))['default'] CodeViewer: (typeof import('./CodeViewer/index.vue'))['default'] ColorPicker: (typeof import('./ColorPicker/index.vue'))['default'] Confirm: (typeof import('./Confirm/index.vue'))['default'] CustomAction: (typeof import('./CustomAction/index.vue'))['default'] Divider: (typeof import('./Divider/index.vue'))['default'] Dropdown: (typeof import('./Dropdown/index.vue'))['default'] Empty: (typeof import('./Empty/index.vue'))['default'] Icon: (typeof import('./Icon/index.vue'))['default'] Input: (typeof import('./Input/index.vue'))['default'] InputList: (typeof import('./InputList/index.vue'))['default'] InterfaceSelect: (typeof import('./InterfaceSelect/index.vue'))['default'] KeyValueEditor: (typeof import('./KeyValueEditor/index.vue'))['default'] Menu: (typeof import('./Menu/index.vue'))['default'] Message: (typeof import('./Message/index.vue'))['default'] Modal: (typeof import('./Modal/index.vue'))['default'] MultipleSelect: (typeof import('./Select/index.vue'))['default'] Pagination: (typeof import('./Pagination/index.vue'))['default'] Picker: (typeof import('./Picker/index.vue'))['default'] Progress: (typeof import('./Progress/index.vue'))['default'] Prompt: (typeof import('./Prompt/index.vue'))['default'] Radio: (typeof import('./Radio/index.vue'))['default'] Select: (typeof import('./Select/index.vue'))['default'] Switch: (typeof import('./Switch/index.vue'))['default'] Table: (typeof import('./Table/index.vue'))['default'] Tabs: (typeof import('./Tabs/index.vue'))['default'] Tag: (typeof import('./Tag/index.vue'))['default'] Tips: (typeof import('./Tips/index.vue'))['default'] TrafficChart: (typeof import('./TrafficChart/index.vue'))['default'] } } ================================================ FILE: frontend/src/components/index.ts ================================================ import type { Plugin, App, Component } from 'vue' export { default as TitleBar } from './_common/TitleBar.vue' export { default as NavigationBar } from './_common/NavigationBar.vue' export { default as SplashView } from './_common/SplashView.vue' export { default as AboutView } from './_common/AboutView.vue' export { default as CommandView } from './_common/CommandView.vue' const Components = import.meta.glob('./*/index.vue', { eager: true, import: 'default', }) export default { install: (app: App) => { Object.entries(Components).forEach(([path, comp]) => { const name = (path.split('/') as [string, string])[1] app.component(name, comp) }) }, } as Plugin ================================================ FILE: frontend/src/constant/app.ts ================================================ import { Color, ControllerCloseMode, Lang, PluginTrigger, RequestMethod, ScheduledTasksType, Theme, View, WebviewGpuPolicy, WindowStartState, } from '@/enums/app' export const LocalesFilePath = 'data/locales' export const UserFilePath = 'data/user.yaml' export const ProfilesFilePath = 'data/profiles.yaml' export const SubscribesFilePath = 'data/subscribes.yaml' export const RulesetsFilePath = 'data/rulesets.yaml' export const PluginsFilePath = 'data/plugins.yaml' export const ScheduledTasksFilePath = 'data/scheduledtasks.yaml' export const PluginHubFilePath = 'data/.cache/plugin-list.json' export const RulesetHubFilePath = 'data/.cache/ruleset-list.json' export const RollingReleaseDirectory = 'data/rolling-release' export const DefaultFontFamily = 'system-ui, "Microsoft YaHei UI", "Source Han Sans CN", "Twemoji Mozilla", sans-serif' export const Colors = { [Color.Default]: { primary: 'rgb(0, 89, 214)', secondary: 'rgb(5, 62, 142)', }, [Color.Green]: { primary: 'green', secondary: '#025f02', }, [Color.Purple]: { primary: 'purple', secondary: '#6a0f9c', }, [Color.Custom]: { primary: '#000', secondary: '#000', }, } export const LanguageOptions = [ { label: 'settings.lang.zh', value: Lang.ZH }, { label: 'settings.lang.en', value: Lang.EN }, ] export const ViewOptions = [ { label: 'common.grid', value: View.Grid }, { label: 'common.list', value: View.List }, ] export const ControllerCloseModeOptions = [ { label: 'home.controller.closeMode.all', value: ControllerCloseMode.All }, { label: 'home.controller.closeMode.button', value: ControllerCloseMode.Button }, ] export const RequestMethodOptions = [ { label: RequestMethod.Get, value: RequestMethod.Get }, { label: RequestMethod.Post, value: RequestMethod.Post }, { label: RequestMethod.Delete, value: RequestMethod.Delete }, { label: RequestMethod.Put, value: RequestMethod.Put }, { label: RequestMethod.Head, value: RequestMethod.Head }, { label: RequestMethod.Patch, value: RequestMethod.Patch }, ] export const ThemeOptions = [ { label: 'settings.theme.dark', value: Theme.Dark, }, { label: 'settings.theme.light', value: Theme.Light, }, { label: 'settings.theme.auto', value: Theme.Auto, }, ] export const ColorOptions = [ { label: 'settings.color.default', value: Color.Default, }, { label: 'settings.color.green', value: Color.Green, }, { label: 'settings.color.purple', value: Color.Purple, }, { label: 'settings.color.custom', value: Color.Custom, }, ] export const WindowStateOptions = [ { label: 'settings.windowState.normal', value: WindowStartState.Normal }, { label: 'settings.windowState.minimised', value: WindowStartState.Minimised }, ] export const WebviewGpuPolicyOptions = [ { label: 'settings.webviewGpuPolicy.always', value: WebviewGpuPolicy.Always }, { label: 'settings.webviewGpuPolicy.onDemand', value: WebviewGpuPolicy.OnDemand }, { label: 'settings.webviewGpuPolicy.never', value: WebviewGpuPolicy.Never }, ] // vue-draggable-plus config export const DraggableOptions = { animation: 150, } export const PluginsTriggerOptions = [ { label: 'plugin.on::startup', value: PluginTrigger.OnStartup }, { label: 'plugin.on::ready', value: PluginTrigger.OnReady }, { label: 'plugin.on::reload', value: PluginTrigger.OnReload }, { label: 'plugin.on::shutdown', value: PluginTrigger.OnShutdown }, { label: 'plugin.on::manual', value: PluginTrigger.OnManual }, { label: 'plugin.on::generate', value: PluginTrigger.OnGenerate }, { label: 'plugin.on::subscribe', value: PluginTrigger.OnSubscribe }, { label: 'plugin.on::tray::update', value: PluginTrigger.OnTrayUpdate }, { label: 'plugin.on::before::core::start', value: PluginTrigger.OnBeforeCoreStart }, { label: 'plugin.on::core::started', value: PluginTrigger.OnCoreStarted }, { label: 'plugin.on::before::core::stop', value: PluginTrigger.OnBeforeCoreStop }, { label: 'plugin.on::core::stopped', value: PluginTrigger.OnCoreStopped }, ] export const ScheduledTaskOptions = [ { label: 'scheduledtask.update::subscription', value: ScheduledTasksType.UpdateSubscription }, { label: 'scheduledtask.update::ruleset', value: ScheduledTasksType.UpdateRuleset }, { label: 'scheduledtask.update::plugin', value: ScheduledTasksType.UpdatePlugin }, { label: 'scheduledtask.run::plugin', value: ScheduledTasksType.RunPlugin }, { label: 'scheduledtask.run::script', value: ScheduledTasksType.RunScript }, { label: 'scheduledtask.update::all::subscription', value: ScheduledTasksType.UpdateAllSubscription, }, { label: 'scheduledtask.update::all::ruleset', value: ScheduledTasksType.UpdateAllRuleset }, { label: 'scheduledtask.update::all::plugin', value: ScheduledTasksType.UpdateAllPlugin }, ] export const DefaultSubscribeScript = `const onSubscribe = async (proxies, subscription) => {\n return { proxies, subscription }\n}` export const DefaultTestURL = 'https://www.gstatic.com/generate_204' export const DefaultTestTimeout = 5000 export const DefaultConcurrencyLimit = 20 export const DefaultCardColumns = 5 export const DefaultControllerSensitivity = 2 ================================================ FILE: frontend/src/constant/kernel.ts ================================================ import { ClashMode, Inbound, Outbound, TunStack, LogLevel, RuleType, RulesetFormat, RulesetType, RuleAction, Sniffer, Strategy, RuleActionReject, DnsServer, } from '@/enums/kernel' export const CoreStopOutputKeyword = 'sing-box started' export const CoreWorkingDirectory = 'data/sing-box' export const CorePidFilePath = CoreWorkingDirectory + '/pid.txt' export const CoreConfigFilePath = CoreWorkingDirectory + '/config.json' export const CoreCacheFilePath = CoreWorkingDirectory + '/cache.db' export const ModeOptions = [ { label: 'kernel.global', value: ClashMode.Global, desc: 'kernel.globalDesc', }, { label: 'kernel.rule', value: ClashMode.Rule, desc: 'kernel.ruleDesc', }, { label: 'kernel.direct', value: ClashMode.Direct, desc: 'kernel.directDesc', }, ] export const LogLevelOptions = [ { label: 'kernel.log.trace', value: LogLevel.Trace, }, { label: 'kernel.log.debug', value: LogLevel.Debug, }, { label: 'kernel.log.info', value: LogLevel.Info, }, { label: 'kernel.log.warn', value: LogLevel.Warn, }, { label: 'kernel.log.error', value: LogLevel.Error, }, { label: 'kernel.log.fatal', value: LogLevel.Fatal, }, { label: 'kernel.log.panic', value: LogLevel.Panic, }, ] export const InboundOptions = [ { label: 'mixed', value: Inbound.Mixed }, { label: 'socks', value: Inbound.Socks }, { label: 'http', value: Inbound.Http }, { label: 'tun', value: Inbound.Tun }, ] export const OutboundOptions = [ { label: 'kernel.outbounds.direct', value: Outbound.Direct }, { label: 'kernel.outbounds.block', value: Outbound.Block }, { label: 'kernel.outbounds.selector', value: Outbound.Selector }, { label: 'kernel.outbounds.urltest', value: Outbound.Urltest }, ] export const RulesTypeOptions = [ { label: 'kernel.rules.type.inbound', value: RuleType.Inbound, }, { label: 'kernel.rules.type.network', value: RuleType.Network, }, { label: 'kernel.rules.type.protocol', value: RuleType.Protocol, }, { label: 'kernel.rules.type.domain', value: RuleType.Domain, }, { label: 'kernel.rules.type.domain_suffix', value: RuleType.DomainSuffix, }, { label: 'kernel.rules.type.domain_keyword', value: RuleType.DomainKeyword, }, { label: 'kernel.rules.type.domain_regex', value: RuleType.DomainRegex, }, { label: 'kernel.rules.type.source_ip_cidr', value: RuleType.SourceIPCidr, }, { label: 'kernel.rules.type.ip_cidr', value: RuleType.IPCidr, }, { label: 'kernel.rules.type.ip_is_private', value: RuleType.IpIsPrivate, }, { label: 'kernel.rules.type.source_port', value: RuleType.SourcePort, }, { label: 'kernel.rules.type.source_port_range', value: RuleType.SourcePortRange, }, { label: 'kernel.rules.type.port', value: RuleType.Port, }, { label: 'kernel.rules.type.port_range', value: RuleType.PortRange, }, { label: 'kernel.rules.type.process_name', value: RuleType.ProcessName, }, { label: 'kernel.rules.type.process_path', value: RuleType.ProcessPath, }, { label: 'kernel.rules.type.process_path_regex', value: RuleType.ProcessPathRegex, }, { label: 'kernel.rules.type.clash_mode', value: RuleType.ClashMode, }, { label: 'kernel.rules.type.rule_set', value: RuleType.RuleSet, }, { label: 'kernel.rules.type.inline', value: RuleType.Inline, }, ] export const DnsRuleTypeOptions = RulesTypeOptions.concat([ { label: 'kernel.rules.type.ip_accept_any', value: RuleType.IpAcceptAny, }, ]) export const TunStackOptions = [ { label: 'kernel.inbounds.tun.system', value: TunStack.System }, { label: 'kernel.inbounds.tun.gvisor', value: TunStack.GVisor }, { label: 'kernel.inbounds.tun.mixed', value: TunStack.Mixed }, ] export const RulesetTypeOptions = [ { label: 'kernel.route.rule_set.type.inline', value: RulesetType.Inline }, { label: 'kernel.route.rule_set.type.local', value: RulesetType.Local }, { label: 'kernel.route.rule_set.type.remote', value: RulesetType.Remote }, ] export const RulesetFormatOptions = [ { label: 'ruleset.format.source', value: RulesetFormat.Source }, { label: 'ruleset.format.binary', value: RulesetFormat.Binary }, ] export const DomainStrategyOptions = [ { label: 'kernel.strategy.default', value: Strategy.Default }, { label: 'kernel.strategy.prefer_ipv4', value: Strategy.PreferIPv4 }, { label: 'kernel.strategy.prefer_ipv6', value: Strategy.PreferIPv6 }, { label: 'kernel.strategy.ipv4_only', value: Strategy.IPv4Only }, { label: 'kernel.strategy.ipv6_only', value: Strategy.IPv6Only }, ] export const RuleActionOptions = [ { label: 'kernel.route.rules.action.route', value: RuleAction.Route }, { label: 'kernel.route.rules.action.route-options', value: RuleAction.RouteOptions }, { label: 'kernel.route.rules.action.reject', value: RuleAction.Reject }, { label: 'kernel.route.rules.action.hijack-dns', value: RuleAction.HijackDNS }, { label: 'kernel.route.rules.action.sniff', value: RuleAction.Sniff }, { label: 'kernel.route.rules.action.resolve', value: RuleAction.Resolve }, ] export const RuleActionRejectOptions = [ { label: 'kernel.route.rules.action.rejectDefault', value: RuleActionReject.Default }, { label: 'kernel.route.rules.action.rejectDrop', value: RuleActionReject.Drop }, { label: 'kernel.route.rules.action.rejectReply', value: RuleActionReject.Reply }, ] export const DnsServerTypeOptions = [ { label: 'kernel.dns.type.local', value: DnsServer.Local }, { label: 'kernel.dns.type.hosts', value: DnsServer.Hosts }, { label: 'kernel.dns.type.tcp', value: DnsServer.Tcp }, { label: 'kernel.dns.type.udp', value: DnsServer.Udp }, { label: 'kernel.dns.type.tls', value: DnsServer.Tls }, { label: 'kernel.dns.type.quic', value: DnsServer.Quic }, { label: 'kernel.dns.type.https', value: DnsServer.Https }, { label: 'kernel.dns.type.h3', value: DnsServer.H3 }, { label: 'kernel.dns.type.dhcp', value: DnsServer.Dhcp }, { label: 'kernel.dns.type.fakeip', value: DnsServer.FakeIP }, ] export const DnsRuleActionOptions = [ { label: 'kernel.route.rules.action.route', value: RuleAction.Route }, { label: 'kernel.route.rules.action.route-options', value: RuleAction.RouteOptions }, { label: 'kernel.route.rules.action.reject', value: RuleAction.Reject }, { label: 'kernel.route.rules.action.predefined', value: RuleAction.Predefined }, ] export const DnsRuleActionRejectOptions = [ { label: 'kernel.route.rules.action.rejectDefault', value: RuleActionReject.Default }, { label: 'kernel.route.rules.action.rejectDrop', value: RuleActionReject.Drop }, ] export const RuleSnifferOptions = [ { label: 'kernel.route.rules.sniffer.http', value: Sniffer.Http }, { label: 'kernel.route.rules.sniffer.tls', value: Sniffer.Tls }, { label: 'kernel.route.rules.sniffer.quic', value: Sniffer.Quic }, { label: 'kernel.route.rules.sniffer.stun', value: Sniffer.Stun }, { label: 'kernel.route.rules.sniffer.dns', value: Sniffer.Dns }, { label: 'kernel.route.rules.sniffer.bittorrent', value: Sniffer.Bittorrent }, { label: 'kernel.route.rules.sniffer.dtls', value: Sniffer.Dtls }, { label: 'kernel.route.rules.sniffer.ssh', value: Sniffer.Ssh }, { label: 'kernel.route.rules.sniffer.rdp', value: Sniffer.Rdp }, { label: 'kernel.route.rules.sniffer.ntp', value: Sniffer.Ntp }, ] export const EmptyRuleSet = { version: 1, rules: [], } export const DefaultExcludeProtocols = 'direct|reject|selector|urltest|block|dns|shadowsocksr' export const BuiltInOutbound = [Outbound.Direct, Outbound.Block] export const DefaultConnections = () => { return { visibility: { 'metadata.type': true, 'metadata.processPath': false, 'metadata.host': true, 'metadata.sourceIP': false, 'metadata.destinationIP': false, rule: true, chains: true, up: true, down: true, upload: true, download: true, start: true, }, order: [ 'metadata.type', 'metadata.processPath', 'metadata.host', 'metadata.sourceIP', 'metadata.destinationIP', 'rule', 'chains', 'up', 'down', 'upload', 'download', 'start', ], } } export const DefaultCoreConfig = () => { return { env: {}, args: [ 'run', '--disable-color', '-c', '$APP_BASE_PATH/$CORE_BASE_PATH/config.json', '-D', '$APP_BASE_PATH/$CORE_BASE_PATH', ], } } ================================================ FILE: frontend/src/constant/profile.ts ================================================ import { LogLevel, Inbound, Outbound, TunStack, ClashMode, RulesetType, RulesetFormat, RuleType, RuleAction, Strategy, DnsServer, } from '@/enums/kernel' import i18n from '@/lang' import { generateSecureKey, sampleID } from '@/utils' import { DefaultTestURL } from './app' const { t } = i18n.global const DefaultOutboundIds = { Select: 'outbound-select', Urltest: 'outbound-urlte', Direct: 'outbound-direct', Block: 'outbound-block', Fallback: 'outbound-fallback', Global: 'outbound-global', } const DefaultInboundIds = { MixedIn: 'mixed-in', Tun: 'tun-in', } const DefaultRulesetIds = { CATEGORY_ADS: 'Category-Ads', GEOIP_CN: 'GeoIP-CN', GEOSITE_CN: 'GeoSite-CN', GEOLOCATION_NOT_CN: 'GeoLocation-!CN', GEOSITE_PRIVATE: 'GeoSite-Private', GEOIP_PRIVATE: 'GeoIP-Private', } const DefaultDnsServersIds = { LocalDns: 'Local-DNS', RemoteDns: 'Remote-DNS', FakeIP: 'Fake-IP', LocalDnsResolver: 'Local-DNS-Resolver', RemoteDnsResolver: 'Remote-DNS-Resolver', } export const DefaultLog = (): ILog => ({ disabled: false, level: LogLevel.Info, output: '', timestamp: false, }) export const DefaultExperimental = (): IExperimental => ({ clash_api: { external_controller: '127.0.0.1:20123', external_ui: '', external_ui_download_url: '', external_ui_download_detour: DefaultOutboundIds.Direct, secret: generateSecureKey(), default_mode: ClashMode.Rule, access_control_allow_origin: ['*'], access_control_allow_private_network: false, }, cache_file: { enabled: true, path: 'cache.db', cache_id: sampleID(), store_fakeip: true, store_rdrc: true, rdrc_timeout: '7d', }, }) export const DefaultInboundSocks = (): NonNullable => ({ listen: { listen: '127.0.0.1', listen_port: 20120, tcp_fast_open: false, tcp_multi_path: false, udp_fragment: false, }, users: [], }) export const DefaultInboundHttp = (): NonNullable => ({ listen: { listen: '127.0.0.1', listen_port: 20121, tcp_fast_open: false, tcp_multi_path: false, udp_fragment: false, }, users: [], }) export const DefaultInboundMixed = (): NonNullable => ({ listen: { listen: '127.0.0.1', listen_port: 20122, tcp_fast_open: false, tcp_multi_path: false, udp_fragment: false, }, users: [], }) export const DefaultInboundTun = (): NonNullable => ({ interface_name: '', address: ['172.18.0.1/30', 'fdfe:dcba:9876::1/126'], mtu: 0, auto_route: true, strict_route: true, route_address: [], route_exclude_address: [], endpoint_independent_nat: false, stack: TunStack.Mixed, }) export const DefaultInbounds = (): IInbound[] => [ { id: DefaultInboundIds.MixedIn, type: Inbound.Mixed, tag: 'mixed-in', enable: true, mixed: DefaultInboundMixed(), }, { id: DefaultInboundIds.Tun, type: Inbound.Tun, tag: 'tun-in', enable: false, tun: DefaultInboundTun(), }, ] export const DefaultOutbound = (): IOutbound => ({ id: sampleID(), tag: '', type: Outbound.Selector, outbounds: [], interrupt_exist_connections: true, url: DefaultTestURL, interval: '3m', tolerance: 150, include: '', exclude: '', }) export const DefaultOutbounds = (): IOutbound[] => [ { id: DefaultOutboundIds.Select, tag: t('outbound.select'), type: Outbound.Selector, outbounds: [{ id: DefaultOutboundIds.Urltest, type: 'Built-in', tag: t('outbound.urltest') }], interrupt_exist_connections: true, url: '', interval: '3m', tolerance: 150, include: '', exclude: '', }, { id: DefaultOutboundIds.Urltest, tag: t('outbound.urltest'), type: Outbound.Urltest, outbounds: [], interrupt_exist_connections: true, url: DefaultTestURL, interval: '3m', tolerance: 150, include: '', exclude: '', }, { id: DefaultOutboundIds.Direct, tag: t('outbound.direct'), type: Outbound.Selector, outbounds: [ { id: 'direct', type: 'Built-in', tag: 'direct' }, { id: 'block', type: 'Built-in', tag: 'block' }, ], interrupt_exist_connections: true, url: '', interval: '3m', tolerance: 150, include: '', exclude: '', }, { id: DefaultOutboundIds.Block, tag: t('outbound.block'), type: Outbound.Selector, outbounds: [ { id: 'block', type: 'Built-in', tag: 'block' }, { id: 'direct', type: 'Built-in', tag: 'direct' }, ], interrupt_exist_connections: true, url: '', interval: '3m', tolerance: 150, include: '', exclude: '', }, { id: DefaultOutboundIds.Fallback, tag: t('outbound.fallback'), type: Outbound.Selector, outbounds: [ { id: DefaultOutboundIds.Select, type: 'Built-in', tag: t('outbound.select') }, { id: DefaultOutboundIds.Direct, type: 'Built-in', tag: t('outbound.direct') }, ], interrupt_exist_connections: true, url: '', interval: '3m', tolerance: 150, include: '', exclude: '', }, { id: DefaultOutboundIds.Global, tag: 'GLOBAL', type: Outbound.Selector, outbounds: [ { id: DefaultOutboundIds.Select, type: 'Built-in', tag: t('outbound.select') }, { id: DefaultOutboundIds.Urltest, type: 'Built-in', tag: t('outbound.urltest') }, { id: DefaultOutboundIds.Direct, type: 'Built-in', tag: t('outbound.direct') }, { id: DefaultOutboundIds.Block, type: 'Built-in', tag: t('outbound.block') }, { id: DefaultOutboundIds.Fallback, type: 'Built-in', tag: t('outbound.fallback') }, ], interrupt_exist_connections: true, url: '', interval: '3m', tolerance: 150, include: '', exclude: '', }, ] export const DefaultRouteRule = (): IRule => ({ id: sampleID(), type: RuleType.RuleSet, enable: true, payload: '', invert: false, action: RuleAction.Route, outbound: '', sniffer: [], strategy: Strategy.Default, server: '', }) export const DefaultRouteRuleset = (): IRuleSet => ({ id: sampleID(), type: RulesetType.Local, tag: '', format: RulesetFormat.Binary, url: '', download_detour: '', update_interval: '', rules: '', path: '', }) export const DefaultRoute = (): IRoute => ({ rules: [ { id: sampleID(), type: RuleType.Inbound, payload: DefaultInboundIds.Tun, enable: true, invert: false, action: RuleAction.Sniff, outbound: '', sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.Protocol, enable: true, payload: 'dns', invert: false, action: RuleAction.HijackDNS, outbound: '', sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.ClashMode, payload: ClashMode.Direct, enable: true, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Direct, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.ClashMode, enable: true, payload: ClashMode.Global, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Global, sniffer: [], strategy: Strategy.Default, server: '', }, { id: RuleType.InsertionPoint, type: RuleType.InsertionPoint, enable: true, payload: '', invert: false, action: RuleAction.Route, outbound: '', sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.Network, enable: true, payload: 'icmp', invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Direct, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.Protocol, enable: true, payload: 'quic', invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Block, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.CATEGORY_ADS, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Block, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOSITE_PRIVATE, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Direct, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOSITE_CN, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Direct, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOIP_PRIVATE, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Direct, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOIP_CN, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Direct, sniffer: [], strategy: Strategy.Default, server: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOLOCATION_NOT_CN, invert: false, action: RuleAction.Route, outbound: DefaultOutboundIds.Select, sniffer: [], strategy: Strategy.Default, server: '', }, ], rule_set: [ { id: DefaultRulesetIds.CATEGORY_ADS, type: RulesetType.Remote, tag: DefaultRulesetIds.CATEGORY_ADS, format: RulesetFormat.Binary, url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs', download_detour: DefaultOutboundIds.Direct, update_interval: '', rules: '', path: '', }, { id: DefaultRulesetIds.GEOIP_PRIVATE, type: RulesetType.Remote, tag: DefaultRulesetIds.GEOIP_PRIVATE, format: RulesetFormat.Binary, url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/private.srs', download_detour: DefaultOutboundIds.Direct, update_interval: '', rules: '', path: '', }, { id: DefaultRulesetIds.GEOSITE_PRIVATE, type: RulesetType.Remote, tag: DefaultRulesetIds.GEOSITE_PRIVATE, format: RulesetFormat.Binary, url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs', download_detour: DefaultOutboundIds.Direct, update_interval: '', rules: '', path: '', }, { id: DefaultRulesetIds.GEOIP_CN, type: RulesetType.Remote, tag: DefaultRulesetIds.GEOIP_CN, format: RulesetFormat.Binary, url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs', download_detour: DefaultOutboundIds.Direct, update_interval: '', rules: '', path: '', }, { id: DefaultRulesetIds.GEOSITE_CN, type: RulesetType.Remote, tag: DefaultRulesetIds.GEOSITE_CN, format: RulesetFormat.Binary, url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs', download_detour: DefaultOutboundIds.Direct, update_interval: '', rules: '', path: '', }, { id: DefaultRulesetIds.GEOLOCATION_NOT_CN, type: RulesetType.Remote, tag: DefaultRulesetIds.GEOLOCATION_NOT_CN, format: RulesetFormat.Binary, url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs', download_detour: DefaultOutboundIds.Direct, update_interval: '', rules: '', path: '', }, ], auto_detect_interface: true, default_interface: '', final: DefaultOutboundIds.Fallback, find_process: false, default_domain_resolver: { server: DefaultDnsServersIds.LocalDns, client_subnet: '', }, }) export const DefaultDnsServer = (): IDNSServer => ({ id: sampleID(), tag: '', type: DnsServer.Local, detour: '', domain_resolver: '', server: '', server_port: '', path: '', interface: '', inet4_range: '', inet6_range: '', hosts_path: [], predefined: {}, }) export const DefaultDnsServers = (): IDNSServer[] => [ { id: DefaultDnsServersIds.FakeIP, tag: DefaultDnsServersIds.FakeIP, detour: '', type: DnsServer.FakeIP, domain_resolver: '', server: '', server_port: '', path: '', interface: '', inet4_range: '198.18.0.0/15', inet6_range: 'fc00::/18', hosts_path: [], predefined: {}, }, { id: DefaultDnsServersIds.LocalDns, tag: DefaultDnsServersIds.LocalDns, detour: '', type: DnsServer.Https, domain_resolver: DefaultDnsServersIds.LocalDnsResolver, server: '223.5.5.5', server_port: '443', path: '/dns-query', interface: '', inet4_range: '', inet6_range: '', hosts_path: [], predefined: {}, }, { id: DefaultDnsServersIds.LocalDnsResolver, tag: DefaultDnsServersIds.LocalDnsResolver, detour: '', type: DnsServer.Udp, domain_resolver: '', server: '223.5.5.5', server_port: '53', path: '', interface: '', inet4_range: '', inet6_range: '', hosts_path: [], predefined: {}, }, { id: DefaultDnsServersIds.RemoteDns, tag: DefaultDnsServersIds.RemoteDns, detour: DefaultOutboundIds.Select, type: DnsServer.Tls, domain_resolver: DefaultDnsServersIds.RemoteDnsResolver, server: '8.8.8.8', server_port: '853', path: '', interface: '', inet4_range: '', inet6_range: '', hosts_path: [], predefined: {}, }, { id: DefaultDnsServersIds.RemoteDnsResolver, tag: DefaultDnsServersIds.RemoteDnsResolver, detour: DefaultOutboundIds.Select, type: DnsServer.Udp, domain_resolver: '', server: '8.8.8.8', server_port: '53', path: '', interface: '', inet4_range: '', inet6_range: '', hosts_path: [], predefined: {}, }, ] export const DefaultFakeIPDnsRule = () => ({ __is_fake_ip: true, type: 'logical', mode: 'and', rules: [ { domain_suffix: [ '.lan', '.localdomain', '.example', '.invalid', '.localhost', '.test', '.local', '.home.arpa', '.msftconnecttest.com', '.msftncsi.com', ], invert: true, }, { query_type: ['A', 'AAAA'], }, ], }) export const DefaultDnsRule = (): IDNSRule => ({ id: sampleID(), type: RuleType.RuleSet, enable: true, payload: '', action: RuleAction.Route, invert: false, // route server: '', strategy: Strategy.Default, // route/route-options disable_cache: false, client_subnet: '', }) export const DefaultDnsRules = (): IDNSRule[] => [ { id: sampleID(), type: RuleType.ClashMode, enable: true, payload: ClashMode.Direct, action: RuleAction.Route, server: DefaultDnsServersIds.LocalDns, invert: false, strategy: Strategy.Default, disable_cache: false, client_subnet: '', }, { id: sampleID(), type: RuleType.ClashMode, enable: true, payload: ClashMode.Global, action: RuleAction.Route, server: DefaultDnsServersIds.RemoteDns, invert: false, strategy: Strategy.Default, disable_cache: false, client_subnet: '', }, { id: RuleType.InsertionPoint, type: RuleType.InsertionPoint, enable: true, payload: '', action: RuleAction.Route, server: '', invert: false, strategy: Strategy.Default, disable_cache: false, client_subnet: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOSITE_CN, action: RuleAction.Route, server: DefaultDnsServersIds.LocalDns, invert: false, strategy: Strategy.Default, disable_cache: false, client_subnet: '', }, { id: sampleID(), type: RuleType.Inline, enable: false, payload: JSON.stringify(DefaultFakeIPDnsRule(), null, 2), action: RuleAction.Route, server: DefaultDnsServersIds.FakeIP, invert: false, strategy: Strategy.Default, disable_cache: false, client_subnet: '', }, { id: sampleID(), type: RuleType.RuleSet, enable: true, payload: DefaultRulesetIds.GEOLOCATION_NOT_CN, action: RuleAction.Route, server: DefaultDnsServersIds.RemoteDns, invert: false, strategy: Strategy.Default, disable_cache: false, client_subnet: '', }, ] export const DefaultDns = (): IDNS => ({ servers: DefaultDnsServers(), rules: DefaultDnsRules(), disable_cache: false, disable_expire: false, independent_cache: false, client_subnet: '', final: DefaultDnsServersIds.RemoteDns, strategy: Strategy.Default, }) export const DefaultMixin = (): IProfile['mixin'] => { return { priority: 'mixin', format: 'json', config: '' } } export const DefaultScript = (): IProfile['script'] => { return { code: `const onGenerate = async (config) => {\n return config\n}` } } ================================================ FILE: frontend/src/directives/index.ts ================================================ import { vDraggable } from 'vue-draggable-plus' import menu from './menu' import platform from './platform' import tips from './tips' import type { Plugin, App } from 'vue' const directives: any = { menu, tips, platform, draggable: vDraggable, } export default { install(app: App) { Object.keys(directives).forEach((key) => { app.directive(key, directives[key]) }) }, } as Plugin ================================================ FILE: frontend/src/directives/menu.ts ================================================ import { useAppStore } from '@/stores' import { sleep } from '@/utils' import type { Directive, DirectiveBinding } from 'vue' const updateMenus = (el: any, binding: DirectiveBinding) => { const appStore = useAppStore() el.oncontextmenu = async (e: MouseEvent) => { e.preventDefault() if (binding.value.length) { appStore.menuPosition = { x: e.clientX, y: e.clientY } appStore.menuList = binding.value if (appStore.menuShow) { appStore.menuShow = false await sleep(200) } appStore.menuShow = true } } } export default { mounted(el: any, binding: DirectiveBinding) { updateMenus(el, binding) }, updated(el: any, binding: DirectiveBinding) { updateMenus(el, binding) }, } as Directive ================================================ FILE: frontend/src/directives/platform.ts ================================================ import { useEnvStore } from '@/stores' import type { Directive, DirectiveBinding } from 'vue' export default { mounted(el: any, binding: DirectiveBinding) { const envStore = useEnvStore() const supports = binding.value if (!supports.includes(envStore.env.os)) { el.remove() } }, } as Directive ================================================ FILE: frontend/src/directives/tips.ts ================================================ import { type Directive, type DirectiveBinding } from 'vue' import { useAppStore } from '@/stores' import { debounce } from '@/utils' export default { mounted(el: HTMLElement, binding: DirectiveBinding) { const appStore = useAppStore() const delay = binding.modifiers.fast ? 200 : 500 const show = debounce((x: number, y: number) => { if (el.dataset.showTips === 'true') { appStore.tipsPosition = { x, y } appStore.tipsMessage = binding.value appStore.tipsShow = true } }, delay) el.onmouseenter = (e: MouseEvent) => { if (binding.value) { el.dataset.showTips = 'true' show(e.clientX, e.clientY) } } el.onmouseleave = () => { appStore.tipsShow = false el.dataset.showTips = 'false' } }, beforeUnmount(el: HTMLElement) { const appStore = useAppStore() appStore.tipsShow = false el.dataset.showTips = 'false' }, } as Directive ================================================ FILE: frontend/src/enums/app.ts ================================================ export enum WindowStartState { Normal = 0, Minimised = 2, } export enum WebviewGpuPolicy { Always = 0, OnDemand = 1, Never = 2, } export enum Theme { Auto = 'auto', Light = 'light', Dark = 'dark', } export enum Lang { EN = 'en', ZH = 'zh', } export enum View { Grid = 'grid', List = 'list', } export enum ControllerCloseMode { All = 'all', Button = 'button', } export enum Color { Default = 'default', Green = 'green', Purple = 'purple', Custom = 'custom', } export enum Branch { Main = 'main', Alpha = 'alpha', } export enum ScheduledTasksType { UpdateSubscription = 'update::subscription', UpdateRuleset = 'update::ruleset', UpdatePlugin = 'update::plugin', UpdateAllSubscription = 'update::all::subscription', UpdateAllRuleset = 'update::all::ruleset', UpdateAllPlugin = 'update::all::plugin', RunPlugin = 'run::plugin', RunScript = 'run::script', } export enum PluginTrigger { OnManual = 'on::manual', OnSubscribe = 'on::subscribe', OnGenerate = 'on::generate', OnStartup = 'on::startup', OnShutdown = 'on::shutdown', OnReady = 'on::ready', OnReload = 'on::reload', OnCoreStarted = 'on::core::started', OnCoreStopped = 'on::core::stopped', OnBeforeCoreStart = 'on::before::core::start', OnBeforeCoreStop = 'on::before::core::stop', OnTrayUpdate = 'on::tray::update', } export enum PluginTriggerEvent { OnInstall = 'onInstall', OnUninstall = 'onUninstall', OnManual = 'onRun', OnTrayUpdate = 'onTrayUpdate', OnSubscribe = 'onSubscribe', OnGenerate = 'onGenerate', OnStartup = 'onStartup', OnShutdown = 'onShutdown', OnReady = 'onReady', OnReload = 'onReload', OnTask = 'onTask', OnConfigure = 'onConfigure', OnCoreStarted = 'onCoreStarted', OnCoreStopped = 'onCoreStopped', OnBeforeCoreStart = 'onBeforeCoreStart', OnBeforeCoreStop = 'onBeforeCoreStop', } export enum RequestMethod { Get = 'GET', Post = 'POST', Delete = 'DELETE', Put = 'PUT', Head = 'HEAD', Patch = 'PATCH', } ================================================ FILE: frontend/src/enums/kernel.ts ================================================ export enum LogLevel { Trace = 'trace', Debug = 'debug', Info = 'info', Warn = 'warn', Error = 'error', Fatal = 'fatal', Panic = 'panic', } export enum ClashMode { Global = 'global', Rule = 'rule', Direct = 'direct', } export enum Inbound { Mixed = 'mixed', Socks = 'socks', Http = 'http', Tun = 'tun', } export enum Outbound { Direct = 'direct', Block = 'block', Selector = 'selector', Urltest = 'urltest', } export enum TunStack { System = 'system', GVisor = 'gvisor', Mixed = 'mixed', } export enum RulesetType { Inline = 'inline', Local = 'local', Remote = 'remote', } export enum RulesetFormat { Source = 'source', Binary = 'binary', } export enum RuleType { Inbound = 'inbound', Network = 'network', Protocol = 'protocol', Domain = 'domain', DomainSuffix = 'domain_suffix', DomainKeyword = 'domain_keyword', DomainRegex = 'domain_regex', SourceIPCidr = 'source_ip_cidr', IPCidr = 'ip_cidr', IpIsPrivate = 'ip_is_private', SourcePort = 'source_port', SourcePortRange = 'source_port_range', Port = 'port', PortRange = 'port_range', ProcessName = 'process_name', ProcessPath = 'process_path', ProcessPathRegex = 'process_path_regex', ClashMode = 'clash_mode', RuleSet = 'rule_set', IpAcceptAny = 'ip_accept_any', // GUI Inline = 'inline', InsertionPoint = 'InsertionPoint', } export enum Strategy { Default = 'default', PreferIPv4 = 'prefer_ipv4', PreferIPv6 = 'prefer_ipv6', IPv4Only = 'ipv4_only', IPv6Only = 'ipv6_only', } export enum DnsServer { Local = 'local', Hosts = 'hosts', Tcp = 'tcp', Udp = 'udp', Tls = 'tls', Https = 'https', Quic = 'quic', H3 = 'h3', Dhcp = 'dhcp', FakeIP = 'fakeip', } export enum RuleAction { Route = 'route', RouteOptions = 'route-options', Reject = 'reject', HijackDNS = 'hijack-dns', Sniff = 'sniff', Resolve = 'resolve', Predefined = 'predefined', } export enum RuleActionReject { Default = 'default', Drop = 'drop', Reply = 'reply', } export enum Sniffer { Http = 'http', Tls = 'tls', Quic = 'quic', Stun = 'stun', Dns = 'dns', Bittorrent = 'bittorrent', Dtls = 'dtls', Ssh = 'ssh', Rdp = 'rdp', Ntp = 'ntp', } ================================================ FILE: frontend/src/hooks/index.ts ================================================ export * from './useBool' ================================================ FILE: frontend/src/hooks/useBool.ts ================================================ import { ref } from 'vue' export const useBool = (initialValue: boolean) => { const value = ref(initialValue) const toggle = () => { value.value = !value.value } return [value, toggle] as const } ================================================ FILE: frontend/src/hooks/useCoreBranch.ts ================================================ import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { Download, HttpCancel, UnzipZIPFile, UnzipTarGZFile, HttpGet, Exec, MoveFile, RemoveFile, AbsolutePath, BrowserOpenURL, MakeDir, FileExists, OpenDir, } from '@/bridge' import { CoreWorkingDirectory } from '@/constant/kernel' import { Branch } from '@/enums/app' import { useAppSettingsStore, useEnvStore, useKernelApiStore } from '@/stores' import { getGitHubApiAuthorization, GrantTUNPermission, ignoredError, confirm, message, debounce, getKernelFileName, getKernelAssetFileName, } from '@/utils' const StableUrl = 'https://api.github.com/repos/SagerNet/sing-box/releases/latest' const AlphaUrl = 'https://api.github.com/repos/SagerNet/sing-box/releases?per_page=3' const StablePage = 'https://github.com/SagerNet/sing-box/releases/latest' const AlphaPage = 'https://github.com/SagerNet/sing-box/releases' export const useCoreBranch = (isAlpha = false) => { const releaseUrl = isAlpha ? AlphaUrl : StableUrl const localVersion = ref('') const remoteVersion = ref('') const versionDetail = ref('') const localVersionLoading = ref(false) const remoteVersionLoading = ref(false) const downloading = ref(false) const downloadCompleted = ref(false) const rollbackable = ref(false) const { t } = useI18n() const envStore = useEnvStore() const appSettings = useAppSettingsStore() const kernelApiStore = useKernelApiStore() const restartable = computed(() => { const { branch } = appSettings.app.kernel if (!kernelApiStore.running) return false return localVersion.value && downloadCompleted.value && (branch === Branch.Alpha) === isAlpha }) const updatable = computed( () => remoteVersion.value && localVersion.value !== remoteVersion.value, ) const grantable = computed(() => localVersion.value && envStore.env.os !== 'windows') const CoreFilePath = `${CoreWorkingDirectory}/${getKernelFileName(isAlpha)}` const CoreBakFilePath = `${CoreFilePath}.bak` const downloadCore = async () => { downloading.value = true try { const { body } = await HttpGet>(releaseUrl, { Authorization: getGitHubApiAuthorization(), }) if (body.message) throw body.message const release = isAlpha ? body.find((v: any) => v.prerelease === true) : body if (!release) throw 'Not Found' const { assets, tag_name } = release const assetName = getKernelAssetFileName(tag_name.replace('v', '')) const asset = assets.find((v: any) => v.name === assetName) if (!asset) throw 'Asset Not Found:' + assetName if (asset.uploader.type !== 'Bot') { await confirm('common.warning', 'settings.kernel.risk', { type: 'text', okText: 'settings.kernel.stillDownload', }) } const downloadCacheFile = `data/.cache/${assetName}` const downloadCancelId = downloadCacheFile const { update, destroy } = message.info('common.downloading', 10 * 60 * 1_000, () => { HttpCancel(downloadCancelId) setTimeout(() => RemoveFile(downloadCacheFile), 1000) }) await MakeDir(CoreWorkingDirectory) await Download( asset.browser_download_url, downloadCacheFile, undefined, (progress, total) => { update(t('common.downloading') + ((progress / total) * 100).toFixed(2) + '%') }, { CancelId: downloadCancelId }, ).finally(destroy) const stableFileName = getKernelFileName() await ignoredError(MoveFile, CoreFilePath, CoreBakFilePath) if (assetName.endsWith('.zip')) { await UnzipZIPFile(downloadCacheFile, 'data/.cache') const tmpPath = `data/.cache/${assetName.replace('.zip', '')}` await MoveFile(`${tmpPath}/${stableFileName}`, CoreFilePath) await RemoveFile(tmpPath) } else if (assetName.endsWith('.tar.gz')) { await UnzipTarGZFile(downloadCacheFile, 'data/.cache') const tmpPath = `data/.cache/${assetName.replace('.tar.gz', '')}` await MoveFile(`${tmpPath}/${stableFileName}`, CoreFilePath) await RemoveFile(tmpPath) } await RemoveFile(downloadCacheFile) if (!CoreFilePath.endsWith('.exe')) { await ignoredError(Exec, 'chmod', ['+x', await AbsolutePath(CoreFilePath)]) } refreshLocalVersion() downloadCompleted.value = true message.success('common.success') } catch (error: any) { console.log(error) message.error(error.message || error) downloadCompleted.value = false } downloading.value = false } const getLocalVersion = async (showTips = false) => { localVersionLoading.value = true try { const res = await Exec(CoreFilePath, ['version']) versionDetail.value = res.trim() return res.match(/version (\S+)/)?.[1] || '' } catch (error: any) { console.log(error) showTips && message.error(error) } finally { localVersionLoading.value = false } return '' } const getRemoteVersion = async (showTips = false) => { remoteVersionLoading.value = true try { const { body } = await HttpGet>(releaseUrl, { Authorization: getGitHubApiAuthorization(), }) const release = isAlpha ? body.find((v: any) => v.prerelease === true) : body if (!release) throw 'Not Found' const { tag_name } = release return tag_name.replace('v', '') as string } catch (error: any) { console.log(error) showTips && message.error(error) } finally { remoteVersionLoading.value = false } return '' } const restartCore = async () => { if (!kernelApiStore.running) return try { await kernelApiStore.restartCore() downloadCompleted.value = false message.success('common.success') } catch (error: any) { message.error(error) } } const refreshLocalVersion = async (showTips = false) => { localVersion.value = await getLocalVersion(showTips) } const refreshRemoteVersion = async (showTips = false) => { remoteVersion.value = await getRemoteVersion(showTips) } const grantCorePermission = async () => { await GrantTUNPermission(CoreFilePath) message.success('common.success') } const rollbackCore = async () => { await confirm('common.warning', 'settings.kernel.rollback') const doRollback = () => MoveFile(CoreBakFilePath, CoreFilePath) const { branch } = appSettings.app.kernel const isCurrentRunning = kernelApiStore.running && (branch === Branch.Alpha) === isAlpha if (isCurrentRunning) { await kernelApiStore.restartCore(doRollback) } else { await doRollback() } refreshLocalVersion() message.success('common.success') } const openReleasePage = () => { BrowserOpenURL(isAlpha ? AlphaPage : StablePage) } const openFileLocation = async () => { await OpenDir(CoreWorkingDirectory) } watch( () => appSettings.app.kernel.branch, () => (downloadCompleted.value = false), ) watch( [localVersion, downloadCompleted], debounce(async () => { rollbackable.value = await FileExists(CoreBakFilePath) }, 500), ) refreshLocalVersion() refreshRemoteVersion() return { restartable, updatable, grantable, rollbackable, versionDetail, localVersion, localVersionLoading, remoteVersion, remoteVersionLoading, downloading, refreshLocalVersion, refreshRemoteVersion, downloadCore, restartCore, rollbackCore, grantCorePermission, openReleasePage, openFileLocation, } } ================================================ FILE: frontend/src/lang/index.ts ================================================ import { createI18n } from 'vue-i18n' import { ReadFile } from '@/bridge' import { LocalesFilePath } from '@/constant/app' import { Lang } from '@/enums/app' const i18n = createI18n({ legacy: false, locale: 'en', fallbackWarn: false, missingWarn: false, messages: {}, }) export const loadLocale = async (locale = i18n.global.locale.value) => { if ([Lang.ZH, Lang.EN].includes(locale as Lang)) { const message = await import(`./locale/${locale}.ts`) i18n.global.setLocaleMessage(locale, message.default) } else { const message = await ReadFile(`${LocalesFilePath}/${locale}.json`).catch(() => '') message && i18n.global.setLocaleMessage(locale, JSON.parse(message)) } } export default i18n ================================================ FILE: frontend/src/lang/locale/en.ts ================================================ export default { common: { grid: 'Grid', list: 'List', add: 'Add', added: 'Added', more: 'More', edit: 'Edit', clear: 'Clear', update: 'Update', delete: 'Delete', cancel: 'Cancel', save: 'Save', nextStep: 'Next', prevStep: 'Back', disabled: 'Disabled', enabled: 'Enabled', preview: 'Preview', warning: 'Warning', disable: 'Disable', enable: 'Enable', use: 'Use', none: 'none', close: 'Close', reset: 'Reset', pause: 'Pause', resume: 'Resume', details: 'Details', updateAll: 'Update All', updateTime: 'Update Time', keywords: 'Keywords', success: 'Success', copy: 'Copy', copied: 'Copied', auto: 'Auto', import: 'Import', install: 'Install', uninstall: 'Uninstall', run: 'Run', refresh: 'Refresh', confirm: 'OK', selectAll: 'Select All', http: 'Remote', file: 'Local', openFile: 'Open File', develop: 'Develop', canceled: 'Canceled', downloading: 'Downloading...', empty: 'Data is empty', pressAgainToClose: 'Press again to close the modal', }, kernel: { rule: 'Rule', global: 'Global', direct: 'Direct', ruleDesc: 'Route traffic based on rules', globalDesc: 'Only follow the Global group', directDesc: 'Directly connect all traffic', log: { disabled: 'Disabled', level: 'Level', output: 'Output', timestamp: 'Timestamp', trace: 'trace', debug: 'debug', info: 'info', warn: 'warn', error: 'error', fatal: 'fatal', panic: 'panic', }, clash_api: { external_controller: 'External Controller', external_ui: 'External UI', external_ui_download_url: 'Web UI Download URL', external_ui_download_detour: 'Web UI Download Detour', secret: 'RESTful API Secret', default_mode: 'Mode', access_control_allow_origin: 'CORS allowed origins', access_control_allow_private_network: 'Allow access from private network', }, cache_file: { enabled: 'Enabled', path: 'Path to the cache file', cache_id: 'Identifier in the cache file', store_fakeip: 'Store Fake-IP', store_rdrc: 'Store Rejected DNS Response', rdrc_timeout: 'Timeout of rejected DNS response cache', }, inbounds: { enable: 'Enable', tag: 'Tag', users: 'Http/Socks users', listen: { listen: 'Listen', listen_port: 'Port', tcp_fast_open: 'TCP Fast Open', tcp_multi_path: 'TCP Multi Path', udp_fragment: 'UDP Fragmentation', }, tun: { interface_name: 'Interface Name', address: 'IPv4 & IPv6 Prefix', mtu: 'MTU', auto_route: 'Auto Route', strict_route: 'Strict Route', route_address: 'Route Address', route_exclude_address: 'Route Exclude Address', endpoint_independent_nat: 'Endpoint Independent NAT', stack: 'Stack', system: 'System', gvisor: 'gVisor', mixed: 'Mixed', }, mixedPort: 'Mixed Port', httpPort: 'HTTP(s) Port', socksPort: 'SOCKS5 Port', }, outbounds: { name: 'Outbound', tag: 'Tag', type: 'Type', url: 'URL', interval: 'Interval(min)', tolerance: 'Tolerance(ms)', interrupt_exist_connections: 'Interrupt Exist Connections', direct: 'Direct', block: 'Block', directDesc: 'No settings', selector: 'Selector', urltest: 'URLTest', notFound: 'Some outbound tags or proxies are missing; please clean them up.', needToAdd: 'At least reference one outbound tag or proxy.', refsSubscription: 'Reference subscription', refsOutbound: 'Reference outbound', sort: 'View and Sort', refs: 'Reference subscription & outbound', noSubs: 'The subscription list is empty.', empty: 'No available proxies under this subscription.', builtIn: 'Built-In', subscriptions: 'Subscriptions', include: 'Include', exclude: 'Exclude', }, route: { tab: { common: 'Common', rules: 'Rules', rule_set: 'Rule-Set', }, find_process: 'Find Process', auto_detect_interface: 'Auto Detect Interface', default_interface: 'Default Interface', final: 'Final Outbound Tag', default_domain_resolver: { server: 'Default Domain Resolver', client_subnet: 'Client Subnet', }, rule_set: { type: { name: 'Name', inline: 'Inline', local: 'Local', remote: 'Remote', }, tag: 'Tag', format: { name: 'Format', binary: 'Binary', source: 'Source', }, url: 'URL', download_detour: 'Download Detour', update_interval: 'Update Interval', path: 'Path', notFound: 'The rule set has been lost.', empty: 'The rule set list is empty.', }, rules: { type: 'Type', action: { name: 'Action', route: 'Route', 'route-options': 'Route-Options', reject: 'Reject', predefined: 'Predefined', 'hijack-dns': 'Hijack-DNS', sniff: 'Sniff', resolve: 'Resolve DNS', rejectMethod: 'Method', rejectDefault: 'default', rejectDrop: 'drop', rejectReply: 'reply', }, outbound: 'Outbound Tag', routeOptions: 'Route Options', sniffer: { name: 'Sniffer', http: 'http', tls: 'tls', quic: 'quic', stun: 'stun', dns: 'dns', bittorrent: 'bittorrent', dtls: 'dtls', ssh: 'ssh', rdp: 'rdp', ntp: 'ntp', }, server: 'DNS Server', payload: 'Payload', strategy: 'Strategy', disable_cache: 'Disable Cache', client_subnet: 'Client Subnet', notFound: 'Outbound tag is missing.', invalid: 'Invalid Parameter', invert: 'Invert', }, }, rules: { type: { name: 'Type', inbound: 'inbound', network: 'network', protocol: 'protocol', domain: 'domain', domain_suffix: 'domain_suffix', domain_keyword: 'domain_keyword', domain_regex: 'domain_regex', source_ip_cidr: 'source_ip_cidr', ip_cidr: 'ip_cidr', ip_is_private: 'ip_is_private', source_port: 'source_port', source_port_range: 'source_port_range', port: 'port', port_range: 'port_range', process_name: 'process_name', process_path: 'process_path', process_path_regex: 'process_path_regex', clash_mode: 'clash_mode', rule_set: 'rule_set', ip_accept_any: 'ip_accept_any', inline: 'Inline', }, }, strategy: { name: 'Strategy', default: 'Default', byDnsRules: 'Determined by DNS rules', prefer_ipv4: 'Prefer IPV4', prefer_ipv6: 'Prefer IPV6', ipv4_only: 'IPV4 Only', ipv6_only: 'IPV6 Only', }, dns: { tab: { common: 'Common', servers: 'Servers', rules: 'Rules', }, tag: 'Tag', type: { name: 'Type', local: 'Local', hosts: 'Hosts', tcp: 'TCP', udp: 'UDP', tls: 'TLS', https: 'HTTPS', quic: 'QUIC', h3: 'HTTP/3', predefined: 'Predefined', dhcp: 'DHCP', fakeip: 'Fake-IP', }, detour: 'Detour', server: 'Server', server_port: 'Server Port', domain_resolver: 'Domain Resolver', path: 'Path', interface: 'Interface', disable_cache: 'Disable Cache', client_subnet: 'Client Subnet', disable_expire: 'Disable Expire', independent_cache: 'Independent Cache', final: 'Final DNS', strategy: 'Strategy', inet4_range: 'Fake-IP Range(IPv4)', inet6_range: 'Fake-IP Range(IPv6)', hosts_path: 'Hosts Path', predefined: 'Predefined', rules: { type: 'Type', payload: 'Payload', action: 'Action', server: 'Server', }, }, mode: 'Mode', 'allow-lan': 'Allow LAN', 'disallow-lan': 'Disallow LAN', notFound: 'Core Not Found', insertionPoint: 'The new rule will be inserted here', addInsertionPoint: 'Add insertion point', }, router: { overview: 'Overview', subscriptions: 'Subscriptions', rulesets: 'Rulesets', plugins: 'Plugins', settings: 'Settings', about: 'About', profiles: 'Profiles', kernel: 'Core', scheduledtasks: 'Tasks', }, home: { mode: 'Proxy Mode', global: 'Global', rule: 'Rule', direct: 'Direct', quickStart: 'Quick Start', noProfile: 'Welcome to the {0}, click the button to get started.', initSuccessful: 'Initialization successful', overview: { expandAll: 'Expand All', collapseAll: 'Collapse All', refresh: 'Refresh', delayTest: 'Delay Test', stop: 'Stop Core', restart: 'Restart Core', viewlog: 'view log', start: 'Click to Start', noLogs: 'Log is empty', systemProxy: 'System Proxy', tunMode: 'TUN Mode', traffic: 'Traffic', realtimeTraffic: 'Real-time Traffic', totalTraffic: 'Total Traffic', connections: 'Connections', memory: 'Memory', transmit: 'Transmit', receive: 'Receive', settings: 'Core Settings', settingsTips: 'Takes effect temporarily. For persistent changes, please modify the `profile` settings.', updateGEO: 'Update GEO', needPort: 'Please add a Mixed/Http/Socks inbound first', needTun: 'Please add a TUN inbound first', }, controller: { name: 'Controller', autoClose: 'Auto-close', unAvailable: 'Show UnAvailable', cardMode: 'Card Mode', sortBy: 'Sort By Delay', delay: 'Latency test URL', timeout: 'Latency test timeout (ms)', concurrencyLimit: 'Latency test concurrency', cardColumns: 'Number of card columns', sensitivity: 'Controller Scroll Sensitivity', closeMode: { name: 'Controller Close Mode', all: 'Scroll or Button', button: 'Button Only', }, }, connections: { type: 'Type', processPath: 'Process Path', sourceIP: 'Source', destinationIP: 'Destination IP', host: 'Host', inbound: 'Inbound', rule: 'Rule', chains: 'Chains', upload: 'Upload', download: 'Download', uploadSpeed: 'UL Speed', downSpeed: 'DL Speed', time: 'Time', close: 'Close', addToDirect: 'Add To DIRECT', addToProxy: 'Add To PROXY', addToReject: 'Add To REJECT', active: 'Active', closed: 'Closed', closeAll: 'Close all connections', sort: 'Sorting and Setting Visibility', details: 'Connection Details', }, }, subscribe: { manual: 'MANUAL', name: 'Name', url: 'Remote Url', localPath: 'Local Path', website: 'Website', path: 'Save Path', include: 'Include Keywords', exclude: 'Exclude Keywords', includeProtocol: 'Include Protocol', excludeProtocol: 'Exclude Protocol', proxyPrefix: 'Proxy Prefix', updating: 'Updating', useragent: 'User-Agent', inSecure: 'Skip TLS Verification', requestMethod: 'Request Method', requestTimeout: 'Request Timeout (seconds)', header: { request: 'Request Header', response: 'Response Header', }, }, subscribes: { download: 'Download', upload: 'Upload', total: 'Total', expire: 'Expire', subtype: 'Subscription Type', website: 'Website', empty: 'The subscription list is empty. Please{action}a subscription first.', enterLink: 'Enter subscription link', proxyCount: 'Proxy Count', editProxies: 'Edit Proxies', editSourceFile: 'Edit Proxies(Source)', copySub: 'Copy Link', script: 'Script', proxies: { type: 'Protocol', name: 'Name', add: 'Add Proxy', }, }, profile: { name: 'Name', generalSettings: 'General Settings', advancedSettings: 'Advanced Settings', step: { name: 'Name', general: 'General', inbounds: 'Inbounds', outbounds: 'Outbounds', route: 'Route', dns: 'DNS', 'mixin-script': 'Mixin & Script', }, proxies: 'Reference proxies', use: 'Reference subscriptions', noSubs: 'There are no available subscriptions.', group: 'Group Details', rule: 'Rule Details', auto: 'This configuration is managed by your subscription and will be overwritten upon update!\nUse the plugin system to make permanent changes.', mixinSettings: { name: 'Mixin', priority: 'Priority', format: 'Format', mixin: 'Mixin', gui: 'GUI', }, scriptSettings: { name: 'Script', }, }, profiles: { shouldStop: 'Unable to delete, this profile is in use.', empty: 'The profiles list is empty, Please{action}a profile first.', copytoClipboard: 'Generate config to clipboard', generateAndView: 'Generate and View', copy: 'Copy and Paste', start: 'Start/Restart with this Profile', inbounds: 'Inbounds', outbounds: 'Outbounds', dnsServers: 'DNS Servers', dnsRules: 'DNS Rules', }, ruleset: { manual: 'MANUAL', format: { name: 'Format', source: 'Source', binary: 'Binary', }, rulesetType: 'Ruleset Type', name: 'Name', url: 'Remote Url', path: 'Save Path', interval: 'Interval', updating: 'Updating', }, rulesets: { hub: 'Ruleset-Hub', total: 'Number of rule-sets', noDesc: 'No description', updating: 'Updating', updateSuccess: 'Ruleset-Hub updated successfully', fetching: 'fetching...', empty: 'The ruleset list is empty. Please{action}or import from the{import}first.', rulesetCount: 'Ruleset Count', editRuleset: 'Edit Rules', selectRuleType: 'Select Rule Type', }, plugin: { trigger: 'Trigger', 'on::manual': 'on::manual', 'on::startup': 'on::startup', 'on::shutdown': 'on::shutdown', 'on::generate': 'on::generate', 'on::subscribe': 'on::subscribe', 'on::ready': 'on::ready', 'on::reload': 'on::reload', 'on::task': 'on::task', 'on::install': 'on::install', 'on::uninstall': 'on::uninstall', 'on::configure': 'on::configure', 'on::core::started': 'on::core::started', 'on::core::stopped': 'on::core::stopped', 'on::before::core::start': 'on::before::core::start', 'on::before::core::stop': 'on::before::core::stop', 'on::tray::update': 'on::tray::update', name: 'Name', version: 'Version', description: 'Description', url: 'Remote Url', install: 'Installation required', installed: 'Installed', path: 'Save Path', type: 'Type', menus: 'Menus', hasUI: 'Has user interface', context: 'Context', configuration: ' Configuration', menuKey: 'Menu Title', menuValue: 'Trigger function name', selectComponent: 'Select a component', confName: 'Name', confDescription: 'Description', confKey: 'Key', confDefault: 'Default', options: 'Options', restore: 'Reset to default', }, plugins: { updating: 'Updating', empty: 'The plugin list is empty. Please{action}or import from the{import}first.', source: 'Source', reload: 'Reload', configuration: 'Configure', hub: 'Plugin-Hub', update: 'Update List', checkForUpdates: 'Check for updates', updateSuccess: 'Plugin-Hub updated successfully', total: 'Number of plug-ins', removeConfiguration: 'Do you want to remove the plugin configuration?', testRun: 'TestRun', deprecated: 'Deprecated', newVersion: 'New', }, scheduledtask: { name: 'Name', type: 'Type', script: 'Script', subscriptions: 'Subscriptions', rulesets: 'Rulesets', plugins: 'Plugins', cron: 'Cron', notification: 'Task Completed Notification', cronTips: 'Seconds Minutes Hours "Day of month" Month "Day of week"', lastTime: 'Last Time', 'update::subscription': 'update::subscription', 'update::ruleset': 'update::ruleset', 'update::plugin': 'update::plugin', 'update::all::subscription': 'update::all::subscription', 'update::all::ruleset': 'update::all::ruleset', 'update::all::plugin': 'update::all::plugin', 'run::plugin': 'run::plugin', 'run::script': 'run::script', }, scheduledtasks: { logs: 'Logs', name: 'Plugin', duration: 'Duration', startTime: 'Start Time', endTime: 'End Time', time: 'Time', result: 'Result', empty: 'The scheduled task list is empty. Please{action}a scheduled task first.', run: 'Run now', log: 'View log', next: 'Next Run Time', }, settings: { personalization: 'Personalization', behavior: 'Behavior', systemProxy: 'System Proxy', advanced: 'Advanced', features: 'Features', general: 'General', theme: { name: 'Theme', light: 'Light Mode', dark: 'Dark Mode', auto: 'System', }, color: { name: 'Color', default: 'Default', green: 'Green', purple: 'Purple', custom: 'Custom', primary: 'Primary', secondary: 'Secondary', }, fontFamily: 'Font-Family', resetFont: 'Reset Font-Family', appFolder: { name: 'App Folder', open: 'Open application folder', }, lang: { name: 'Language', load: 'Load language files', zh: '简体中文', en: 'English', }, pages: { name: 'Page visibility', }, windowState: { normal: 'Normal window', maximised: 'Maximised', minimised: 'Minimize window', fullscreen: 'Fullscreen', }, webviewGpuPolicy: { name: 'Webview Gpu Policy', always: 'Always', onDemand: 'OnDemand', never: 'Never', }, needRestart: 'Restart Required', needAdmin: 'Admin required', exitOnClose: 'Exit on window close', closeKernelOnExit: 'Stop core on exit', autoSetSystemProxy: 'Auto-configure System Proxy', proxyBypassList: 'Proxy Bypass List', proxyBypassListTips: 'Separate with semicolons', autoStartKernel: 'Start core on launch', realMemoryUsage: 'Show actual core memory usage', autoRestartKernel: { name: 'Auto-restart core on config changes', tips: 'It will interrupt all connections and may fail to restart', }, admin: 'Run as admin', addPluginToMenu: 'Add plugin to tray menu', addGroupToMenu: 'Add proxy group to tray menu', multipleInstance: 'Allow multiple app instances', rollingRelease: 'Enable Rolling Release', debugOutline: 'Show component outlines', debugNoAnimation: 'Disable animations', debugNoRounded: 'Disable rounded corners', debugBorder: 'Show window border', startup: { name: 'Run at startup', delay: 'Delay(s)', startupDelay: 'Startup delay', }, kernel: { name: 'sing-box', version: 'Switch version', stable: 'Stable version', alpha: 'Alpha version', grant: 'Grant Privileges', openTip: 'Open File Location', linkTip: 'View on GitHub Releases', local: 'Local', remote: 'Remote', update: 'Update', restart: 'Restart Core', risk: 'This version is not auto-built by GitHub and may pose a security risk.', stillDownload: 'Still download', rollbackTip: 'Rollback to the previous version', rollback: 'Are you sure you want to roll back to the previous version?', clearCache: 'Clear Cache', config: { name: 'Runtime Configuration', env: 'Environment Variables', args: 'Runtime Arguments', }, }, plugin: { resetSetting: 'Reset setting', resetSettings: 'Reset all settings', }, userAgent: { name: 'User-Agent', reset: 'Reset User-Agent', tips: 'Used for this app’s network requests', }, githubapi: { name: 'GitHub REST API Token', tips: 'Provides a higher rate limit', }, }, about: { new: 'New', restart: 'Restart APP', noDownloadLink: 'No download link found', updateSuccessfulRestart: 'Update completed, please restart the App', updateSuccessfulReplace: 'Download completed, please manually replace the App', updateSuccessful: 'Update completed', newVersion: 'New version found', latestVersion: 'Already the latest version', }, titlebar: { resetSize: 'Reset Window', reload: 'Reload Window', restart: 'Restart App', exitApp: 'Exit App', exitPending: 'Waiting for the program to exit...', exitTimeout: 'The program exit timed out. Do you want to force quit?', exitError: 'An error occurred during exit. Do you want to force quit?\n\nReason: {reason}', reloadPending: 'Waiting for the program to reload...', reloadTimeout: 'The reload timed out. Do you want to force reload?', reloadError: 'An error occurred during reload. Do you want to force reload?\n\nReason: {reason}', }, outbound: { select: '🚀 Select', urltest: '🎈 Auto', direct: '🎯 Direct', block: '🛑 Block', fallback: '🐟 Fallback', }, tray: { showMainWindow: 'Show Main Window', restart: 'Restart', restartTip: 'Restart App', exit: 'Exit', exitTip: 'Exit App', proxyGroup: 'Proxy Group', setSystemProxy: 'Set System Proxy', clearSystemProxy: 'Clear System Proxy', tun: 'Tun Mode', enableTunMode: 'Enable Tun Mode', disableTunMode: 'Disable Tun Mode', kernel: 'Core', proxy: 'System Proxy', startKernel: 'Start Core', stopKernel: 'Stop Core', restartKernel: 'Restart Core', plugins: 'Plugins', }, commands: { noMatching: 'No matching commands', }, } ================================================ FILE: frontend/src/lang/locale/zh.ts ================================================ export default { common: { grid: '网格', list: '列表', add: '添加', added: '已添加', more: '更多', edit: '编辑', clear: '清理', update: '更新', delete: '删除', cancel: '取消', save: '保存', nextStep: '下一步', prevStep: '上一步', disabled: '已禁用', enabled: '已启用', preview: '预览', warning: '警告', disable: '禁用', enable: '启用', use: '使用', none: '无', close: '关闭', reset: '重置', pause: '暂停', resume: '恢复', details: '详情', updateAll: '更新全部', updateTime: '更新时间', keywords: '关键词', success: '成功', copy: '复制', copied: '已复制', auto: '自动', import: '导入', install: '安装', uninstall: '卸载', run: '运行', refresh: '刷新', confirm: '确定', selectAll: '全选', http: '远程下载', file: '本地文件', openFile: '打开文件', develop: '开发', canceled: '已取消', downloading: '下载中...', empty: '数据为空', pressAgainToClose: '再按一次关闭弹窗', }, kernel: { rule: '规则', global: '全局', direct: '直连', ruleDesc: '按照规则文件分流', globalDesc: '仅走Global策略组', directDesc: '直接连接所有流量', log: { disabled: '禁用日志', level: '日志级别', output: '日志保存路径', timestamp: '日志时间戳', trace: '跟踪', debug: '调试', info: '信息', warn: '警告', error: '错误', fatal: '致命', panic: '恐慌', }, clash_api: { external_controller: 'RESTful Web API监听地址', external_ui: 'Web UI路径', external_ui_download_url: 'Web UI下载地址', external_ui_download_detour: 'Web UI下载地址的出站标签', secret: 'RESTful API密钥', default_mode: '工作模式', access_control_allow_origin: '允许的CORS来源', access_control_allow_private_network: '允许从私有网络访问', }, cache_file: { enabled: '启用缓存', path: '缓存文件路径', cache_id: '缓存文件中的标识符', store_fakeip: '持久化FakeIP', store_rdrc: '持久化已拒绝的DNS响应', rdrc_timeout: '拒绝的DNS响应缓存超时', }, inbounds: { enable: '启用', tag: '名称', users: 'Http/Socks验证用户', listen: { listen: '监听地址', listen_port: '端口', tcp_fast_open: 'TCP快速打开', tcp_multi_path: '多路径TCP', udp_fragment: 'UDP分段', }, tun: { interface_name: 'TUN网卡名称', address: 'IPv4和IPv6前缀', mtu: '最大传输单元', auto_route: '自动设置全局路由', strict_route: '严格路由', route_address: '自定义路由', route_exclude_address: '排除自定义路由', endpoint_independent_nat: '独立于端点的 NAT', stack: 'TUN模式堆栈', system: 'System', gvisor: 'gVisor', mixed: 'Mixed', }, mixedPort: '混合代理端口', httpPort: 'HTTP(s)代理端口', socksPort: 'SOCKS5代理端口', }, outbounds: { name: '出站', tag: '名称', type: '类型', url: '测延迟链接', interval: '测试间隔(m)', tolerance: '测试容差(ms)', interrupt_exist_connections: '中断现有连接', direct: '直连', block: '阻断', directDesc: '无设置项目', selector: '手动选择', urltest: '自动选择', notFound: '部分出站或代理已丢失,请清理', needToAdd: '至少引用一个出站或订阅', refsSubscription: '引用订阅', refsOutbound: '引用出站', sort: '查看和排序', refs: '引用订阅&引用节点', noSubs: '订阅列表为空', empty: '该订阅下无可用代理', builtIn: '内置', subscriptions: '订阅', include: '包含', exclude: '排除', }, route: { tab: { common: '通用', rules: '规则', rule_set: '规则集', }, find_process: '查找进程信息', auto_detect_interface: '自动检测出站接口', default_interface: '出站接口名称', final: '默认出站标签', default_domain_resolver: { server: '解析节点域名的DNS服务器', client_subnet: '客户端子网', }, rule_set: { type: { name: '类型', inline: '内联', local: '本地', remote: '远程', }, tag: '名称', format: { name: '格式', binary: '二进制', source: '源文件', }, url: '远程链接', download_detour: '下载方式', update_interval: '自动更新间隔', path: '保存路径', notFound: '规则集已丢失', empty: '规则集列表为空', }, rules: { type: '规则类型', action: { name: '规则动作', route: '路由', 'route-options': '路由设置选项', reject: '拒绝连接', predefined: '预定义', 'hijack-dns': '劫持DNS请求', sniff: '协议嗅探', resolve: '解析DNS', rejectMethod: '拒绝方式', rejectDefault: 'default', rejectDrop: 'drop', rejectReply: 'reply', }, outbound: '出站标签', routeOptions: '路由选项', sniffer: { name: '探测器', http: 'http', tls: 'tls', quic: 'quic', stun: 'stun', dns: 'dns', bittorrent: 'bittorrent', dtls: 'dtls', ssh: 'ssh', rdp: 'rdp', ntp: 'ntp', }, server: 'DNS服务器', payload: '载荷', strategy: '解析策略', disable_cache: '禁用DNS缓存', client_subnet: '客户端子网', notFound: '出站标签缺失。', invalid: '无效参数', invert: '反向匹配', }, }, rules: { type: { name: '类型', inbound: '入站(inbound)', network: '网络(network)', protocol: '协议(protocol)', domain: '域名(domain)', domain_suffix: '域名后缀(domain_suffix)', domain_keyword: '域名关键词(domain_keyword)', domain_regex: '域名正则(domain_regex)', source_ip_cidr: '源IP地址段(source_ip_cidr)', ip_cidr: 'IP地址段(ip_cidr)', ip_is_private: '是否为私有IP(ip_is_private)', source_port: '源端口(source_port)', source_port_range: '源端口范围(source_port_range)', port: '端口(port)', port_range: '端口范围(port_range)', process_name: '进程名称(process_name)', process_path: '进程路径(process_path)', process_path_regex: '进程路径正则(process_path_regex)', clash_mode: 'Clash模式(clash_mode)', rule_set: '规则集(rule_set)', ip_accept_any: '匹配任意 IP(ip_accept_any)', inline: '内联(Inline)', }, }, strategy: { name: '策略', default: '默认', byDnsRules: '由DNS路由规则决定', prefer_ipv4: 'IPV4优先', prefer_ipv6: 'IPV6优先', ipv4_only: '只使用IPV4', ipv6_only: '只使用IPV6', }, dns: { tab: { common: '通用', servers: '服务器', rules: '规则', }, tag: '名称', type: { name: '类型', local: '本地', hosts: 'Hosts', tcp: 'TCP', udp: 'UDP', tls: 'TLS', https: 'HTTPS', quic: 'QUIC', h3: 'HTTP/3', predefined: '预定义', dhcp: 'DHCP', fakeip: 'Fake-IP', }, detour: '出站标签', server: '地址', server_port: '端口', domain_resolver: '解析本DNS服务器域名的DNS', path: '路径', interface: '接口名称', disable_cache: '禁用DNS缓存', client_subnet: '客户端子网', disable_expire: '禁用DNS缓存过期', independent_cache: '独立缓存', final: '回退DNS', strategy: '解析策略', inet4_range: 'Fake-IP范围(IPv4)', inet6_range: 'Fake-IP范围(IPv6)', hosts_path: 'Hosts文件路径', predefined: '预定义', rules: { type: '类型', payload: '载荷', action: '规则动作', server: '目标DNS服务器的标签', }, }, mode: '工作模式', 'allow-lan': '允许局域网访问', 'disallow-lan': '禁止局域网访问', notFound: '无核心', insertionPoint: '新规则将插入到这里', addInsertionPoint: '添加插入点', }, router: { overview: '概览', subscriptions: '订阅', rulesets: '规则集', plugins: '插件', settings: '设置', about: '关于', profiles: '配置', kernel: '核心', scheduledtasks: '计划任务', }, home: { mode: '代理模式', global: '全局', rule: '规则', direct: '直连', quickStart: '快速开始', noProfile: '欢迎使用 {0},点击按钮开始。', initSuccessful: '初始化配置、订阅成功', overview: { expandAll: '展开全部', collapseAll: '收缩全部', refresh: '刷新', delayTest: '延迟测试', stop: '停止核心', restart: '重启核心', viewlog: '查看日志', start: '启动核心', noLogs: '日志为空', systemProxy: '系统代理', tunMode: 'TUN模式', traffic: '流量', realtimeTraffic: '实时流量', totalTraffic: '总流量', connections: '活动连接', memory: '内存', transmit: '上行速率', receive: '下行速率', settings: '核心设置', settingsTips: '暂时生效,持久化请修改配置文件', updateGEO: '更新 GEO', needPort: '请先添加一个Mixed/Http/Socks入站', needTun: '请先添加一个TUN入站', }, controller: { name: '控制器', autoClose: '自动断开连接', unAvailable: '展示不可用节点', cardMode: '卡片模式', sortBy: '按延迟排序', delay: '延迟测试URL', timeout: '延迟测试超时时间(ms)', concurrencyLimit: '延迟测试并发数量', cardColumns: '卡片展示列数', sensitivity: '控制器滚动灵敏度', closeMode: { name: '控制器关闭模式', all: '滚动和关闭按钮', button: '仅按钮', }, }, connections: { type: '类型', processPath: '进程路径', sourceIP: '源地址', destinationIP: '目标IP', host: '主机', inbound: '入站模式', rule: '匹配规则', chains: '链路', upload: '上行流量', download: '下行流量', uploadSpeed: '上行速度', downSpeed: '下行速度', time: '连接时间', close: '关闭连接', addToDirect: '添加到直连', addToProxy: '添加到代理', addToReject: '添加到拦截', active: '活动', closed: '已关闭', closeAll: '关闭所有连接', sort: '排序和设置显示字段', details: '连接详情', }, }, subscribe: { manual: '手动管理', name: '名称', url: '远程链接', localPath: '本地路径', website: '官网', path: '保存路径', include: '包括名称', exclude: '排除名称', includeProtocol: '包括协议', excludeProtocol: '排除协议', proxyPrefix: '代理前缀', updating: '更新中', useragent: '用户代理', inSecure: '跳过证书验证', requestMethod: '请求方式', requestTimeout: '请求超时时间(秒)', header: { request: '请求头', response: '响应头', }, }, subscribes: { download: '下行流量', upload: '上行流量', total: '总流量', expire: '过期时间', subtype: '订阅类型', website: '官网', empty: '订阅列表为空,请先{action}订阅。', enterLink: '输入订阅链接', proxyCount: '代理数量', editProxies: '编辑节点', editSourceFile: '编辑节点(源文件)', copySub: '复制订阅链接', script: '脚本', proxies: { type: '协议', name: '名称', add: '添加代理', }, }, profile: { name: '名称', generalSettings: '通用设置', advancedSettings: '高级设置', step: { name: '名称设置', general: '通用设置', inbounds: '入站设置', outbounds: '出站设置', route: '路由设置', dns: 'DNS设置', 'mixin-script': '混入和脚本', }, proxies: '引用节点', use: '引用订阅', noSubs: '没有可用的订阅', group: '策略组详情', rule: '规则详情', auto: '此配置由订阅接管,更新订阅时会被覆盖!\n如果你想修改此配置,请使用插件系统。', mixinSettings: { name: '混入配置', priority: '优先级', format: '格式', mixin: '混入优先', gui: 'GUI优先', }, scriptSettings: { name: '脚本操作', }, }, profiles: { shouldStop: '当前配置正在使用,无法删除', empty: '配置列表为空,请先{action}配置。', copytoClipboard: '生成配置到剪切板', generateAndView: '生成配置并查看', copy: '复制并粘贴', start: '使用此配置启动/重启', inbounds: '入站', outbounds: '出站', dnsServers: 'DNS服务器', dnsRules: 'DNS规则', }, ruleset: { manual: '手动管理', format: { name: '文件格式', source: '源文件', binary: '二进制', }, rulesetType: '规则集类型', name: '名称', url: '远程链接', path: '保存路径', interval: '更新间隔', updating: '更新中', }, rulesets: { hub: '规则集中心', total: '规则集数量为', noDesc: '无描述信息', updating: '更新中', updateSuccess: '规则集中心更新成功', fetching: '获取中...', empty: '规则集列表为空,请先{action}或从{import}导入。', rulesetCount: '规则数量', editRuleset: '编辑规则', selectRuleType: '选择规则类型', }, plugin: { trigger: '触发器', 'on::manual': '手动触发', 'on::startup': '启动APP时', 'on::shutdown': '关闭APP时', 'on::generate': '生成配置时', 'on::subscribe': '更新订阅时', 'on::ready': 'APP就绪后', 'on::reload': 'APP重载时', 'on::task': '计划任务执行时', 'on::install': '点击安装时', 'on::uninstall': '点击卸载时', 'on::configure': '配置插件时', 'on::core::started': '核心启动后', 'on::core::stopped': '核心停止后', 'on::before::core::start': '核心启动前', 'on::before::core::stop': '核心停止前', 'on::tray::update': '托盘更新时', name: '名称', version: '版本号', description: '描述', url: '远程地址', install: '是否需要安装', installed: '已安装', path: '保存路径', type: '类型', menus: '菜单', hasUI: '是否具有用户界面', context: '上下文', configuration: '配置', menuKey: '菜单名称', menuValue: '触发方法名', selectComponent: '请选择一个组件', confName: '配置名', confDescription: '配置描述', confKey: '配置标志', confDefault: '默认值', options: '选项', restore: '恢复为默认值', }, plugins: { updating: '更新中', empty: '插件列表为空,请先{action}或从{import}导入。', source: '源码', reload: '重载插件', configuration: '配置插件', hub: '插件中心', update: '更新列表', checkForUpdates: '检查更新', updateSuccess: '插件中心更新成功', total: '插件数量为', removeConfiguration: '是否删除插件配置?', testRun: '运行测试', deprecated: '已废弃', newVersion: '新版本', }, scheduledtask: { name: '名称', type: '任务类型', script: '脚本代码', subscriptions: '订阅列表', rulesets: '规则集列表', plugins: '插件列表', cron: '表达式', notification: '任务完成通知', cronTips: '秒 分 时 日 月 星期', lastTime: '上次执行时间', 'update::subscription': '更新订阅', 'update::ruleset': '更新规则集', 'update::plugin': '更新插件', 'update::all::subscription': '更新所有订阅', 'update::all::ruleset': '更新所有规则集', 'update::all::plugin': '更新所有插件', 'run::plugin': '运行插件', 'run::script': '运行脚本', }, scheduledtasks: { logs: '日志', name: '插件', duration: '持续时间', startTime: '开始时间', endTime: '结束时间', time: '执行时间', result: '执行结果', empty: '计划任务列表为空,请先{action}计划任务。', run: '立即运行', log: '查看日志', next: '下次运行时间', }, settings: { personalization: '个性化', behavior: '行为', systemProxy: '系统代理', advanced: '高级', features: '特性', general: '通用', theme: { name: '主题', light: '浅色', dark: '深色', auto: '跟随系统', }, color: { name: '颜色', default: '默认', green: '绿色', purple: '紫色', custom: '自定义', primary: '主色', secondary: '辅助色', }, fontFamily: '字体', resetFont: '重置字体', appFolder: { name: '应用程序文件夹', open: '打开应用程序文件夹', }, lang: { name: '语言', load: '加载语言文件', zh: '简体中文', en: 'English', }, pages: { name: '页面可见性', }, windowState: { normal: '以普通窗口启动', maximised: '最大化', minimised: '最小化窗口启动', fullscreen: '全屏', }, webviewGpuPolicy: { name: 'Webview GPU 策略', always: '启用硬件加速', onDemand: '根据Web内容自行决定', never: '禁用硬件加速', }, needRestart: '重启生效', needAdmin: '需要管理员权限', exitOnClose: '关闭窗口时退出程序', closeKernelOnExit: '程序退出时关闭核心', autoSetSystemProxy: '自动配置系统代理', proxyBypassList: '不使用代理的地址', proxyBypassListTips: '分号分隔', autoStartKernel: '程序启动时开启核心', realMemoryUsage: '显示真实的核心内存占用', autoRestartKernel: { name: '相关配置变化时自动重启核心', tips: '会中断所有连接,且可能重启失败', }, admin: '以管理员身份运行', addPluginToMenu: '将插件添加到托盘菜单', addGroupToMenu: '将代理组添加到托盘菜单', multipleInstance: '允许多APP实例运行', rollingRelease: '启用滚动发行', debugOutline: '组件轮廓', debugNoAnimation: '禁用动画', debugNoRounded: '禁用圆角', debugBorder: '显示窗口边框', startup: { name: '自启动', delay: '延迟(秒)', startupDelay: '自启动延迟', }, kernel: { name: 'sing-box', version: '切换版本', stable: '稳定版', alpha: '内测版', grant: '授予特权', openTip: '打开文件所在位置', linkTip: '在GitHub上查看发布版本', local: '本地', remote: '远程', update: '更新', restart: '重启核心', risk: '该版本非GitHub自动构建,有安全风险。', stillDownload: '仍要下载', rollbackTip: '回滚到上一版本', rollback: '确定回滚到上一版本吗?', clearCache: '清除缓存', config: { name: '运行时配置', env: '环境变量', args: '运行参数', }, }, plugin: { resetSetting: '重置设置', resetSettings: '重置所有设置', }, userAgent: { name: '用户代理(User-Agent)', reset: ' 重置用户代理', tips: '用于此应用程序的网络请求', }, githubapi: { name: 'GitHub REST API 访问令牌', tips: '可获得更高的速率限制', }, }, about: { new: '新版本', restart: '重启软件', noDownloadLink: '没有发现下载链接', updateSuccessfulRestart: '更新完成,请重启软件', updateSuccessfulReplace: '下载完成,请手动替换软件', updateSuccessful: '更新完成', newVersion: '发现新版本', latestVersion: '已经是最新版本了', }, titlebar: { resetSize: '重置窗口', reload: '重载界面', restart: '重启程序', exitApp: '退出程序', exitPending: '正在等待程序退出...', exitTimeout: '程序退出超时,是否强制退出?', exitError: '退出时发生错误,是否强制退出?\n\n原因:{reason}', reloadPending: '正在等待程序重载...', reloadTimeout: '程序重载超时,是否强制重载?', reloadError: '重载时发生错误,是否强制重载?\n\n原因:{reason}', }, outbound: { select: '🚀 节点选择', urltest: '🎈 自动选择', direct: '🎯 全球直连', block: '🛑 全球拦截', fallback: '🐟 漏网之鱼', }, tray: { showMainWindow: '显示主窗口', restart: '重启', restartTip: '重启程序', exit: '退出', exitTip: '退出程序', proxyGroup: '代理组', setSystemProxy: '设置系统代理', clearSystemProxy: '清除系统代理', tun: 'Tun模式', enableTunMode: '启用TUN模式', disableTunMode: '禁用TUN模式', kernel: '核心管理', proxy: '系统代理', startKernel: '开启核心', stopKernel: '关闭核心', restartKernel: '重启核心', plugins: '插件', }, commands: { noMatching: '没有匹配到命令', }, } ================================================ FILE: frontend/src/main.ts ================================================ import { createPinia } from 'pinia' import { createApp } from 'vue' import './assets/main.less' import './assets/polyfills' import './assets/globalMethods' import App from './App.vue' import components from './components' import directives from './directives' import i18n from './lang' import router from './router' const app = createApp(App) window.appInstance = app app.use(createPinia()) app.use(router) app.use(i18n) app.use(components) app.use(directives) app.mount('#app') ================================================ FILE: frontend/src/router/index.ts ================================================ import { createRouter, createWebHashHistory } from 'vue-router' import routes from './routes' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes, }) export default router ================================================ FILE: frontend/src/router/router.d.ts ================================================ import 'vue-router' import { type IconType } from '@/components/Icon/index.vue' declare module 'vue-router' { interface RouteMeta { name: string icon?: IconType hidden?: boolean } } ================================================ FILE: frontend/src/router/routes.ts ================================================ import { type RouteRecordRaw } from 'vue-router' const routes: RouteRecordRaw[] = [ { path: '/', name: 'Overview', component: () => import('@/views/HomeView/index.vue'), meta: { name: 'router.overview', icon: 'overview', }, }, { path: '/profiles', name: 'Profiles', component: () => import('@/views/ProfilesView/index.vue'), meta: { name: 'router.profiles', icon: 'profiles', }, }, { path: '/subscriptions', name: 'Subscriptions', component: () => import('@/views/SubscribesView/index.vue'), meta: { name: 'router.subscriptions', icon: 'subscriptions', }, }, { path: '/rulesets', name: 'Rulesets', component: () => import('@/views/RulesetsView/index.vue'), meta: { name: 'router.rulesets', icon: 'rulesets', }, }, { path: '/plugins', name: 'Plugins', component: () => import('@/views/PluginsView/index.vue'), meta: { name: 'router.plugins', icon: 'plugins', }, }, { path: '/scheduledtasks', name: 'ScheduledTasks', component: () => import('@/views/ScheduledTasksView/index.vue'), meta: { name: 'router.scheduledtasks', icon: 'scheduledTasks', }, }, { path: '/settings', name: 'Settings', component: () => import('@/views/SettingsView/index.vue'), meta: { name: 'router.settings', icon: 'settings2', hidden: false, }, }, ] export default routes ================================================ FILE: frontend/src/stores/app.ts ================================================ import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import { Download, HttpGet, MoveFile, UnzipZIPFile, MakeDir, RemoveFile, HttpCancel, OpenDir, ReadDir, } from '@/bridge' import { LanguageOptions, LocalesFilePath, RollingReleaseDirectory } from '@/constant/app' import { loadLocale } from '@/lang' import { APP_TITLE, APP_VERSION, APP_VERSION_API, getGitHubApiAuthorization, ignoredError, message, alert, sampleID, sleep, } from '@/utils' import { useEnvStore } from './env' import type { CustomAction, CustomActionFn, Menu } from '@/types/app' export const useAppStore = defineStore('app', () => { const isAppExiting = ref(false) const isAppReloading = ref(false) /* Global Menu */ const menuShow = ref(false) const menuList = ref([]) const menuPosition = ref({ x: 0, y: 0, }) /* Global Tips */ const tipsShow = ref(false) const tipsMessage = ref('') const tipsPosition = ref({ x: 0, y: 0, }) /* Modal Stack */ const modalStack: (() => void)[] = [] const modalZIndexCounter = 999 /* i18n */ const localesLoading = ref(false) const locales = ref<{ label: string; value: string }[]>([]) const loadLocales = async (delay = true, reload = true) => { localesLoading.value = true const dirs = await ReadDir(LocalesFilePath).catch(() => []) const localLanguage = dirs.flatMap((file) => { if (file.isDir) return [] const [name, ext] = file.name.split('.') return name && ext === 'json' ? { label: name, value: name } : [] }) locales.value = [...LanguageOptions, ...localLanguage] reload && (await loadLocale()) delay && (await sleep(200)) localesLoading.value = false } /* Actions */ const customActions = ref({ core_state: [] as (CustomAction | CustomActionFn)[], title_bar: [] as (CustomAction | CustomActionFn)[], profiles_header: [] as (CustomAction | CustomActionFn)[], subscriptions_header: [] as (CustomAction | CustomActionFn)[], }) const addCustomActions = ( target: keyof typeof customActions.value, actions: CustomAction | CustomAction[] | CustomActionFn | CustomActionFn[], ) => { if (!customActions.value[target]) throw new Error('Target does not exist: ' + target) const _actions = Array.isArray(actions) ? actions : [actions] _actions.forEach((action) => !action.id && (action.id = sampleID())) customActions.value[target].push(..._actions) const remove = () => { customActions.value[target] = customActions.value[target].filter( (a) => !_actions.some((added) => added.id === a.id), ) } return remove } const removeCustomActions = (target: keyof typeof customActions.value, id: string | string[]) => { if (!customActions.value[target]) throw new Error('Target does not exist: ' + target) const ids = Array.isArray(id) ? id : [id] customActions.value[target] = customActions.value[target].filter((a) => !ids.includes(a.id!)) } const { t } = useI18n() const envStore = useEnvStore() /* About Page */ const showAbout = ref(false) const checkForUpdatesLoading = ref(false) const restartable = ref(false) const downloading = ref(false) const downloadUrl = ref('') const remoteVersion = ref(APP_VERSION) const updatable = computed(() => downloadUrl.value && APP_VERSION !== remoteVersion.value) const downloadApp = async () => { downloading.value = true try { const downloadCacheFile = 'data/.cache/gui.zip' const downloadCancelId = downloadCacheFile const { update, destroy } = message.info('common.downloading', 10 * 60 * 1_000, () => { HttpCancel(downloadCancelId) setTimeout(() => RemoveFile(downloadCacheFile), 1000) }) await MakeDir('data/.cache') await Download( downloadUrl.value, downloadCacheFile, undefined, (progress, total) => { update(t('common.downloading') + ((progress / total) * 100).toFixed(2) + '%') }, { CancelId: downloadCancelId, }, ).finally(destroy) const { appName, os } = envStore.env if (os !== 'darwin') { await MoveFile(appName, appName + '.bak') await UnzipZIPFile(downloadCacheFile, '.') const suffix = { windows: '.exe', linux: '' }[os] await MoveFile(APP_TITLE + suffix, appName) message.success('about.updateSuccessfulRestart') restartable.value = true } else { await UnzipZIPFile(downloadCacheFile, 'data') alert('common.success', 'about.updateSuccessfulReplace') await OpenDir('data') } await RemoveFile(downloadCacheFile) await ignoredError(RemoveFile, RollingReleaseDirectory) } catch (error: any) { console.log(error) message.error(error.message || error, 5_000) } downloading.value = false } const checkForUpdates = async (showTips = false) => { if (checkForUpdatesLoading.value || downloading.value) return checkForUpdatesLoading.value = true remoteVersion.value = APP_VERSION try { const { body } = await HttpGet>(APP_VERSION_API, { Authorization: getGitHubApiAuthorization(), }) if (body.message) throw body.message const { tag_name, assets } = body const { os, arch } = envStore.env const assetName = `${APP_TITLE}-${os}-${arch}.zip` const asset = assets.find((v: any) => v.name === assetName) if (!asset) throw 'Asset Not Found:' + assetName remoteVersion.value = tag_name downloadUrl.value = asset.browser_download_url if (showTips) { message.info(updatable.value ? 'about.newVersion' : 'about.latestVersion') } } catch (error: any) { console.error(error) message.error(error.message || error) } checkForUpdatesLoading.value = false } return { isAppExiting, isAppReloading, menuShow, menuPosition, menuList, tipsShow, tipsMessage, tipsPosition, modalStack, modalZIndexCounter, showAbout, checkForUpdatesLoading, restartable, downloading, remoteVersion, updatable, checkForUpdates, downloadApp, customActions, addCustomActions, removeCustomActions, localesLoading, locales, loadLocales, } }) ================================================ FILE: frontend/src/stores/appSettings.ts ================================================ import { defineStore } from 'pinia' import { ref, watch } from 'vue' import { parse, stringify } from 'yaml' import { ReadFile, WriteFile, WindowSetSystemDefaultTheme, WindowIsMaximised, WindowIsMinimised, } from '@/bridge' import { Colors, DefaultCardColumns, DefaultConcurrencyLimit, DefaultControllerSensitivity, DefaultFontFamily, DefaultTestTimeout, DefaultTestURL, UserFilePath, } from '@/constant/app' import { DefaultConnections, DefaultCoreConfig } from '@/constant/kernel' import { Theme, WindowStartState, Lang, View, Color, WebviewGpuPolicy, ControllerCloseMode, Branch, } from '@/enums/app' import i18n, { loadLocale } from '@/lang' import { useAppStore, useEnvStore } from '@/stores' import { debounce, updateTrayAndMenus, ignoredError, GetSystemProxyBypass, deepClone, } from '@/utils' import type { AppSettings } from '@/types/app' export const useAppSettingsStore = defineStore('app-settings', () => { const appStore = useAppStore() const envStore = useEnvStore() let latestUserSettings: string const app = ref({ lang: Lang.EN, theme: Theme.Auto, color: Color.Default, primaryColor: '#000', secondaryColor: '#545454', fontFamily: DefaultFontFamily, profilesView: View.Grid, subscribesView: View.Grid, rulesetsView: View.Grid, pluginsView: View.Grid, scheduledtasksView: View.Grid, windowStartState: WindowStartState.Normal, webviewGpuPolicy: WebviewGpuPolicy.OnDemand, width: 0, height: 0, exitOnClose: true, closeKernelOnExit: true, autoSetSystemProxy: true, proxyBypassList: '', autoStartKernel: false, autoRestartKernel: false, userAgent: '', startupDelay: 30, connections: DefaultConnections(), kernel: { realMemoryUsage: false, branch: Branch.Main, profile: '', autoClose: true, unAvailable: true, cardMode: true, cardColumns: DefaultCardColumns, sortByDelay: false, testUrl: DefaultTestURL, testTimeout: DefaultTestTimeout, concurrencyLimit: DefaultConcurrencyLimit, controllerCloseMode: ControllerCloseMode.All, controllerSensitivity: DefaultControllerSensitivity, main: undefined as any, alpha: undefined as any, }, pluginSettings: {}, githubApiToken: '', multipleInstance: false, addPluginToMenu: false, addGroupToMenu: false, rollingRelease: true, debugOutline: false, debugNoAnimation: false, debugNoRounded: false, debugBorder: false, pages: ['Overview', 'Profiles', 'Subscriptions', 'Plugins'], }) const saveAppSettings = debounce((config: string) => { WriteFile(UserFilePath, config) }, 500) const setupAppSettings = async () => { const data = await ignoredError(ReadFile, UserFilePath) let settings: AppSettings if (data) { settings = parse(data) } else { settings = deepClone(app.value) } await appStore.loadLocales(false, false) if (!settings.kernel.main) { settings.kernel.main = DefaultCoreConfig() settings.kernel.alpha = DefaultCoreConfig() } if (!settings.proxyBypassList) { settings.proxyBypassList = await GetSystemProxyBypass() } app.value = settings latestUserSettings = stringify(app.value) } const applyAppSettings = { theme(theme: Theme) { const isAuto = theme === Theme.Auto if (isAuto) { themeMode.value = mediaQueryList.matches ? Theme.Dark : Theme.Light } else { themeMode.value = theme } }, lang(lang: string) { i18n.global.locale.value = lang if (!i18n.global.availableLocales.includes(lang)) { loadLocale(lang) } }, color(color: Color, primary: string, secondary: string) { if (color !== Color.Custom) { ;({ primary, secondary } = Colors[color] ?? { primary, secondary }) } document.documentElement.style.setProperty('--primary-color', primary) document.documentElement.style.setProperty('--secondary-color', secondary) }, feature(outline: boolean, noAnimation: boolean, noRounded: boolean, border: boolean) { document.body.setAttribute('feature-outline', String(outline)) document.body.setAttribute('feature-no-animation', String(noAnimation)) document.body.setAttribute('feature-no-rounded', String(noRounded)) document.body.setAttribute('feature-border', String(border)) }, fontFamily(fontFamily: string) { document.body.style.fontFamily = fontFamily }, windowSize(width: number, height: number) { app.value.width = width app.value.height = height }, systemProxyBypass() { if (envStore.systemProxy) { envStore.setSystemProxy() } }, } /* Apply AppSettings */ const onAppSettingsChange = (settings: AppSettings) => { applyAppSettings.theme(settings.theme) applyAppSettings.color(settings.color, settings.primaryColor, settings.secondaryColor) applyAppSettings.lang(settings.lang) applyAppSettings.fontFamily(settings.fontFamily) applyAppSettings.feature( settings.debugOutline, settings.debugNoAnimation, settings.debugNoRounded, settings.debugBorder, ) const lastModifiedSettings = stringify(settings) if (latestUserSettings !== lastModifiedSettings) { saveAppSettings(lastModifiedSettings).then(() => { latestUserSettings = lastModifiedSettings }) } else { saveAppSettings.cancel() } } watch(app, onAppSettingsChange, { deep: true }) /* Apply AppTheme */ const themeMode = ref(Theme.Light) const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)') mediaQueryList.addEventListener('change', ({ matches }) => { if (app.value.theme === Theme.Auto) { themeMode.value = matches ? Theme.Dark : Theme.Light } }) const setAppTheme = (theme: Theme.Dark | Theme.Light) => { if (document.startViewTransition) { document.startViewTransition(() => { document.body.setAttribute('theme-mode', theme) }) } else { document.body.setAttribute('theme-mode', theme) } WindowSetSystemDefaultTheme() } watch(themeMode, setAppTheme, { immediate: true }) /* Apply WindowSize */ const onWindowSizeChange = debounce(async () => { const [isMinimised, isMaximised] = await Promise.all([WindowIsMinimised(), WindowIsMaximised()]) if (!isMinimised && !isMaximised) { const w = document.documentElement.clientWidth const h = document.documentElement.clientHeight applyAppSettings.windowSize(w, h) } }, 1000) window.addEventListener('resize', onWindowSizeChange) /* Apply TrayAndMenus */ watch( [ themeMode, appStore.locales, () => app.value.color, () => app.value.lang, () => app.value.addPluginToMenu, ], updateTrayAndMenus, ) /* Apply SystemProxyBypass */ const setSystemProxyBypass = debounce(() => { applyAppSettings.systemProxyBypass() }, 3000) watch(() => app.value.proxyBypassList, setSystemProxyBypass) return { setupAppSettings, app, themeMode } }) ================================================ FILE: frontend/src/stores/env.ts ================================================ import { defineStore } from 'pinia' import { ref, watch } from 'vue' import { GetEnv } from '@/bridge' import { useAppSettingsStore, useKernelApiStore } from '@/stores' import { updateTrayAndMenus, SetSystemProxy, GetSystemProxy } from '@/utils' export const useEnvStore = defineStore('env', () => { const appSettings = useAppSettingsStore() const kernelApiStore = useKernelApiStore() const env = ref({ appName: '', appVersion: '', basePath: '', appPath: '', os: '', arch: '', isPrivileged: false, }) const systemProxy = ref(false) const setupEnv = async () => { const _env = await GetEnv() const appPath = `${_env.basePath}/${_env.appName}` env.value = { ..._env, appPath: _env.os === 'windows' ? appPath.replaceAll('/', '\\') : appPath, } } const updateSystemProxyStatus = async () => { const kernelApiStore = useKernelApiStore() const proxyServer = await GetSystemProxy() if (!proxyServer) { systemProxy.value = false } else { const { port, 'mixed-port': mixedPort, 'socks-port': socksPort } = kernelApiStore.config const proxyServerList = [ `http://127.0.0.1:${port}`, `http://127.0.0.1:${mixedPort}`, `socks5://127.0.0.1:${mixedPort}`, `socks5://127.0.0.1:${socksPort}`, `socks=127.0.0.1:${mixedPort}`, `socks=127.0.0.1:${socksPort}`, ] systemProxy.value = proxyServerList.includes(proxyServer) } return systemProxy.value } const setSystemProxy = async () => { const proxyBypassList = appSettings.app.proxyBypassList let proxyPort = kernelApiStore.getProxyPort() if (!proxyPort) { await kernelApiStore.updateConfig('inbound', undefined) } proxyPort = kernelApiStore.getProxyPort() if (!proxyPort) throw 'home.overview.needPort' await SetSystemProxy(true, '127.0.0.1:' + proxyPort.port, proxyPort.proxyType, proxyBypassList) systemProxy.value = true } const clearSystemProxy = async () => { const proxyBypassList = appSettings.app.proxyBypassList await SetSystemProxy(false, '', undefined, proxyBypassList) systemProxy.value = false } const switchSystemProxy = async (enable: boolean) => { if (enable) await setSystemProxy() else await clearSystemProxy() } watch(systemProxy, updateTrayAndMenus) return { env, setupEnv, systemProxy, setSystemProxy, clearSystemProxy, switchSystemProxy, updateSystemProxyStatus, } }) ================================================ FILE: frontend/src/stores/index.ts ================================================ export * from './appSettings' export * from './profiles' export * from './subscribes' export * from './rulesets' export * from './plugins' export * from './scheduledtasks' export * from './logs' export * from './kernelApi' export * from './app' export * from './env' ================================================ FILE: frontend/src/stores/kernelApi.ts ================================================ import { defineStore } from 'pinia' import { computed, ref, watch } from 'vue' import { getProxies, getConfigs, setConfigs, onLogs, onMemory, onConnections, onTraffic, initWebsocket, destroyWebsocket, } from '@/api/kernel' import { ProcessInfo, KillProcess, ExecBackground, ReadFile, RemoveFile } from '@/bridge' import { CoreConfigFilePath, CorePidFilePath, CoreStopOutputKeyword, CoreWorkingDirectory, } from '@/constant/kernel' import { DefaultInboundMixed } from '@/constant/profile' import { Branch } from '@/enums/app' import { Inbound, RulesetType, TunStack } from '@/enums/kernel' import { useAppSettingsStore, useProfilesStore, useLogsStore, useEnvStore, usePluginsStore, useSubscribesStore, useRulesetsStore, } from '@/stores' import { generateConfigFile, updateTrayAndMenus, getKernelFileName, restoreProfile, deepClone, message, getKernelRuntimeArgs, getKernelRuntimeEnv, eventBus, } from '@/utils' import type { CoreApiConfig, CoreApiProxy } from '@/types/kernel' export type ProxyType = 'mixed' | 'http' | 'socks' export const useKernelApiStore = defineStore('kernelApi', () => { const envStore = useEnvStore() const logsStore = useLogsStore() const pluginsStore = usePluginsStore() const profilesStore = useProfilesStore() const subscribesStore = useSubscribesStore() const rulesetsStore = useRulesetsStore() const appSettingsStore = useAppSettingsStore() /** RESTful API */ const config = ref({ port: 0, 'mixed-port': 0, 'socks-port': 0, 'interface-name': '', 'allow-lan': false, mode: '', tun: { enable: false, stack: '', device: '', }, }) let runtimeProfile: IProfile | undefined const proxies = ref>({}) const refreshConfig = async () => { const _config = await getConfigs() config.value = { ..._config, tun: config.value.tun, } if (!runtimeProfile) { const txt = await ReadFile(CoreConfigFilePath) runtimeProfile = restoreProfile(JSON.parse(txt)) const profile = profilesStore.currentProfile if (profile) { const _profile = deepClone(profile) _profile.inbounds.forEach((inbound) => { const runtimeInbound = runtimeProfile?.inbounds.find((v) => v.tag === inbound.tag) if (runtimeInbound) { runtimeInbound.id = inbound.id } else { inbound.enable = false runtimeProfile?.inbounds.push(inbound) } }) runtimeProfile.id = _profile.id runtimeProfile.outbounds = _profile.outbounds runtimeProfile.experimental = _profile.experimental runtimeProfile.dns = _profile.dns runtimeProfile.route = _profile.route runtimeProfile.mixin = _profile.mixin runtimeProfile.script = _profile.script } } const mixed = runtimeProfile.inbounds.find((v) => v.enable && v.mixed) const http = runtimeProfile.inbounds.find((v) => v.enable && v.http) const socks = runtimeProfile.inbounds.find((v) => v.enable && v.socks) const tun = runtimeProfile.inbounds.find((v) => v.tun) config.value['mixed-port'] = mixed?.mixed?.listen.listen_port || 0 config.value['port'] = http?.http?.listen.listen_port || 0 config.value['socks-port'] = socks?.socks?.listen.listen_port || 0 config.value['allow-lan'] = [ mixed?.mixed?.listen.listen, http?.http?.listen.listen, socks?.socks?.listen.listen, ].some((address) => address === '0.0.0.0' || address === '::') config.value.tun.enable = !!tun?.enable config.value.tun.device = tun?.tun?.interface_name || '' config.value.tun.stack = tun?.tun?.stack || '' config.value['interface-name'] = runtimeProfile.route.default_interface } const updateConfig = async (field: string, value: any) => { if (field === 'mode') { await setConfigs({ mode: value }) await refreshConfig() return } const patchInbound = () => { if (!runtimeProfile) return const inbound = runtimeProfile.inbounds.find( (v) => (v.type === Inbound.Mixed && v.mixed?.listen.listen_port) || (v.type === Inbound.Http && v.http?.listen.listen_port) || (v.type === Inbound.Socks && v.socks?.listen.listen_port), ) if (!inbound) { throw 'home.overview.needPort' } inbound.enable = true } const patchInboundPort = (type: 'mixed' | 'socks' | 'http', port: number) => { if (!runtimeProfile) return let inbound = runtimeProfile.inbounds.find((v) => v.type === type) if (inbound) { inbound[type]!.listen.listen_port = port } else { const _type = DefaultInboundMixed()! _type.listen.listen_port = port inbound = { id: type + '-in', tag: type + '-in', type: type, enable: true, [type]: _type, } runtimeProfile.inbounds.push(inbound) } inbound.enable = port !== 0 } const patchInboundAddress = (allowLan: boolean) => { if (!runtimeProfile) return runtimeProfile.inbounds.forEach((inbound) => { if (inbound.type === Inbound.Tun) return inbound[inbound.type]!.listen.listen = allowLan ? '0.0.0.0' : '127.0.0.1' }) } const patchInboundTun = (options: { enable: boolean stack: string device: string interface_name: string }) => { if (!runtimeProfile) return const inbound = runtimeProfile.inbounds.find((v) => v.type === Inbound.Tun) if (!inbound) throw 'home.overview.needTun' options = { ...config.value.tun, ...options } inbound.enable = options.enable inbound.tun!.stack = options.stack || TunStack.Mixed inbound.tun!.interface_name = options.device || '' if (options.interface_name) { runtimeProfile.route.default_interface = options.interface_name } runtimeProfile.route.auto_detect_interface = !options.interface_name } const fieldHandlerMap: Recordable<() => void> = { inbound: () => patchInbound(), http: () => patchInboundPort(Inbound.Http, value), socks: () => patchInboundPort(Inbound.Socks, value), mixed: () => patchInboundPort(Inbound.Mixed, value), 'allow-lan': () => patchInboundAddress(value), tun: () => patchInboundTun(value), 'tun-stack': () => patchInboundTun(value), 'tun-device': () => patchInboundTun(value), 'interface-name': () => patchInboundTun(value), } fieldHandlerMap[field]?.() await restartCore(undefined, true) await envStore.updateSystemProxyStatus() } const refreshProviderProxies = async () => { const { proxies: b } = await getProxies() proxies.value = b } /* Bridge API */ const corePid = ref(-1) const running = ref(false) const starting = ref(false) const stopping = ref(false) const restarting = ref(false) const needRestart = ref(false) const coreStateLoading = ref(true) let isCoreStartedByThisInstance = false let { promise: coreStoppedPromise, resolve: coreStoppedResolver } = Promise.withResolvers() const initCoreState = async () => { corePid.value = Number(await ReadFile(CorePidFilePath).catch(() => -1)) const processName = corePid.value === -1 ? '' : await ProcessInfo(corePid.value).catch(() => '') running.value = processName.startsWith('sing-box') coreStateLoading.value = false if (running.value) { initWebsocket() await Promise.all([refreshConfig(), refreshProviderProxies()]) await envStore.updateSystemProxyStatus() } else if (appSettingsStore.app.autoStartKernel) { await startCore() } } const runCoreProcess = (isAlpha: boolean) => { return new Promise((resolve, reject) => { let output: string const pid = ExecBackground( CoreWorkingDirectory + '/' + getKernelFileName(isAlpha), getKernelRuntimeArgs(isAlpha), (out) => { output = out logsStore.recordKernelLog(out) if (out.includes(CoreStopOutputKeyword)) { resolve(pid) } }, () => { onCoreStopped() reject(output) }, { PidFile: CorePidFilePath, StopOutputKeyword: CoreStopOutputKeyword, Env: getKernelRuntimeEnv(isAlpha), }, ).catch((e) => reject(e)) }) } const onCoreStarted = async (pid: number) => { corePid.value = pid running.value = true needRestart.value = false isCoreStartedByThisInstance = true coreStoppedPromise = new Promise((r) => (coreStoppedResolver = r)) initWebsocket() await Promise.all([refreshConfig(), refreshProviderProxies()]) if (appSettingsStore.app.autoSetSystemProxy) { await envStore.setSystemProxy().catch((err) => message.error(err)) } await envStore.updateSystemProxyStatus() await pluginsStore.onCoreStartedTrigger() } const onCoreStopped = async () => { if (!isCoreStartedByThisInstance) { await RemoveFile(CorePidFilePath) } corePid.value = -1 running.value = false needRestart.value = false destroyWebsocket() await envStore.updateSystemProxyStatus() if (envStore.systemProxy) { await envStore.clearSystemProxy() } await pluginsStore.onCoreStoppedTrigger() coreStoppedResolver(null) } const startCore = async (_profile?: IProfile) => { if (running.value) throw 'The core is already running' logsStore.clearKernelLog() const { profile: profileID, branch } = appSettingsStore.app.kernel const profile = _profile || profilesStore.getProfileById(profileID) if (!profile) throw 'Choose a profile first' if (!_profile) { runtimeProfile = undefined } starting.value = true try { await generateConfigFile(profile, (config) => pluginsStore.onBeforeCoreStartTrigger(config, profile), ) const isAlpha = branch === Branch.Alpha const pid = await runCoreProcess(isAlpha) pid && (await onCoreStarted(pid)) } finally { starting.value = false } } const stopCore = async () => { if (!running.value) throw 'The core is not running' stopping.value = true try { await pluginsStore.onBeforeCoreStopTrigger() await KillProcess(corePid.value) await (isCoreStartedByThisInstance ? coreStoppedPromise : onCoreStopped()) } finally { stopping.value = false } } const restartCore = async (cleanupTask?: () => Promise, keepRuntimeProfile = false) => { restarting.value = true try { await stopCore() await cleanupTask?.() await startCore(keepRuntimeProfile ? runtimeProfile : undefined) } finally { needRestart.value = false restarting.value = false } } const getProxyPort = (): | { port: number proxyType: ProxyType } | undefined => { const { port, 'socks-port': socksPort, 'mixed-port': mixedPort } = config.value if (mixedPort) { return { port: mixedPort, proxyType: 'mixed', } } if (port) { return { port, proxyType: 'http', } } if (socksPort) { return { port: socksPort, proxyType: 'socks', } } return undefined } eventBus.on('profileChange', ({ id }) => { if (running.value && id === appSettingsStore.app.kernel.profile) { needRestart.value = true } }) eventBus.on('subscriptionChange', ({ id }) => { if (running.value && profilesStore.currentProfile) { const inUse = profilesStore.currentProfile.outbounds.some(({ outbounds }) => outbounds.some((outbound) => outbound.type === 'Subscription' && outbound.id === id), ) if (inUse) { needRestart.value = true } } }) eventBus.on('subscriptionsChange', () => { if (running.value && profilesStore.currentProfile) { const enabledSubs = subscribesStore.subscribes.flatMap((v) => (v.disabled ? [] : v.id)) const inUse = profilesStore.currentProfile.outbounds.some(({ outbounds }) => outbounds.some( (outbound) => outbound.type === 'Subscription' && enabledSubs.includes(outbound.id), ), ) if (inUse) { needRestart.value = true } } }) const collectRulesetIDs = () => { if (!profilesStore.currentProfile) return [] const l1 = profilesStore.currentProfile.route.rule_set.flatMap((ruleset) => ruleset.type === RulesetType.Local ? ruleset.path : [], ) return l1 } eventBus.on('rulesetChange', ({ id }) => { if (running.value && profilesStore.currentProfile) { const inUse = profilesStore.currentProfile.route.rule_set.some( (ruleset) => ruleset.type === RulesetType.Local && ruleset.path === id, ) if (inUse) { needRestart.value = true } } }) eventBus.on('rulesetsChange', () => { if (running.value && profilesStore.currentProfile) { const enabledRulesets = rulesetsStore.rulesets.flatMap((v) => (v.disabled ? [] : v.id)) const inUse = collectRulesetIDs().some((v) => enabledRulesets.includes(v)) if (inUse) { needRestart.value = true } } }) watch(needRestart, (v) => { if (v && appSettingsStore.app.autoRestartKernel) { restartCore() } }) const watchSources = computed(() => { const source = [config.value.mode, config.value.tun.enable] if (!appSettingsStore.app.addGroupToMenu) return source.join('') const { unAvailable, sortByDelay } = appSettingsStore.app.kernel const proxySignature = Object.values(proxies.value) .map((group) => group.name + group.now) .sort() .join() return source.concat([proxySignature, unAvailable, sortByDelay]).join('') }) watch([watchSources, running], updateTrayAndMenus) return { startCore, stopCore, restartCore, initCoreState, pid: corePid, running, starting, stopping, restarting, needRestart, coreStateLoading, config, proxies, refreshConfig, updateConfig, refreshProviderProxies, getProxyPort, onLogs, onMemory, onTraffic, onConnections, } }) ================================================ FILE: frontend/src/stores/logs.ts ================================================ import { defineStore } from 'pinia' import { computed, ref } from 'vue' interface TaskLogRecord { name: string startTime: number endTime: number result: T } export const useLogsStore = defineStore('logs', () => { const kernelLogs = ref([]) const scheduledtasksLogs = ref([]) const recordKernelLog = (msg: string) => { kernelLogs.value.unshift(msg) } const recordScheduledTasksLog = (log: TaskLogRecord) => scheduledtasksLogs.value.unshift(log) const isTasksLogEmpty = computed(() => scheduledtasksLogs.value.length === 0) const isEmpty = computed(() => kernelLogs.value.length === 0) const clearKernelLog = () => kernelLogs.value.splice(0) return { recordKernelLog, clearKernelLog, kernelLogs, isEmpty, scheduledtasksLogs, isTasksLogEmpty, recordScheduledTasksLog, } }) ================================================ FILE: frontend/src/stores/plugins.ts ================================================ import { defineStore } from 'pinia' import { computed, ref, watch } from 'vue' import { parse } from 'yaml' import { HttpGet, ReadFile, RemoveFile, WriteFile } from '@/bridge' import { PluginHubFilePath, PluginsFilePath } from '@/constant/app' import { PluginTrigger, PluginTriggerEvent } from '@/enums/app' import { useAppSettingsStore } from '@/stores' import { ignoredError, updateTrayAndMenus, isNumber, omitArray, deepClone, confirm, asyncPool, stringifyNoFolding, readonly, } from '@/utils' import type { Plugin, Subscription, TrayContent, MenuItem } from '@/types/app' const PluginsCache: Recordable<{ plugin: Plugin; code: string }> = {} const PluginsTriggerMap: { [key in PluginTrigger]: { fnName: PluginTriggerEvent observers: string[] } } = { [PluginTrigger.OnManual]: { fnName: PluginTriggerEvent.OnManual, observers: [], }, [PluginTrigger.OnTrayUpdate]: { fnName: PluginTriggerEvent.OnTrayUpdate, observers: [], }, [PluginTrigger.OnSubscribe]: { fnName: PluginTriggerEvent.OnSubscribe, observers: [], }, [PluginTrigger.OnGenerate]: { fnName: PluginTriggerEvent.OnGenerate, observers: [], }, [PluginTrigger.OnStartup]: { fnName: PluginTriggerEvent.OnStartup, observers: [], }, [PluginTrigger.OnShutdown]: { fnName: PluginTriggerEvent.OnShutdown, observers: [], }, [PluginTrigger.OnReady]: { fnName: PluginTriggerEvent.OnReady, observers: [], }, [PluginTrigger.OnReload]: { fnName: PluginTriggerEvent.OnReload, observers: [], }, [PluginTrigger.OnCoreStarted]: { fnName: PluginTriggerEvent.OnCoreStarted, observers: [], }, [PluginTrigger.OnCoreStopped]: { fnName: PluginTriggerEvent.OnCoreStopped, observers: [], }, [PluginTrigger.OnBeforeCoreStart]: { fnName: PluginTriggerEvent.OnBeforeCoreStart, observers: [], }, [PluginTrigger.OnBeforeCoreStop]: { fnName: PluginTriggerEvent.OnBeforeCoreStop, observers: [], }, } export const usePluginsStore = defineStore('plugins', () => { const appSettingsStore = useAppSettingsStore() const plugins = ref([]) const pluginHub = ref([]) const setupPlugins = async () => { const data = await ignoredError(ReadFile, PluginsFilePath) data && (plugins.value = parse(data)) const list = await ignoredError(ReadFile, PluginHubFilePath) list && (pluginHub.value = JSON.parse(list)) for (const plugin of plugins.value) { const { id, triggers, path } = plugin const code = await ignoredError(ReadFile, path) if (code) { PluginsCache[id] = { plugin, code } triggers.forEach((trigger) => { PluginsTriggerMap[trigger].observers.push(id) }) } } } const getPluginMetadata = (id: string) => { const lastConfiguration: Recordable = { time: 0, data: undefined } const buildConfiguration = (plugin: Plugin) => { const now = performance.now() if (lastConfiguration.data && now - lastConfiguration.time < 1000) { return lastConfiguration.data } const configuration: Recordable = {} for (const { key, value } of plugin.configuration) { configuration[key] = value } const userSettings = appSettingsStore.app.pluginSettings[plugin.id] if (userSettings) { for (const key in userSettings) { configuration[key] = userSettings[key] } } lastConfiguration.time = now lastConfiguration.data = configuration return configuration } const lastPlugin: { time: number; data: Plugin | undefined } = { time: 0, data: undefined } const getPlugin = () => { const now = performance.now() if (lastPlugin.data && now - lastPlugin.time < 1000) { return lastPlugin.data } const cache = PluginsCache[id] if (!cache) throw new Error() lastPlugin.time = now lastPlugin.data = cache.plugin return cache.plugin } const proxy = new Proxy({} as Plugin & Recordable, { get(_, p) { const plugin = getPlugin() if (typeof p === 'string' && p.startsWith('__v_')) { return Reflect.get(plugin, p) } let value if (Object.hasOwn(plugin, p)) { value = Reflect.get(plugin, p) } else { const configuration = buildConfiguration(plugin) value = Reflect.get(configuration, p) } if (p === 'status') return value return readonly(value) }, set(_, p, newValue) { const plugin = getPlugin() if (p === 'status') { plugin.status = newValue editPlugin(plugin.id, plugin) return true } console.warn(`[${plugin.name}] Property "${String(p)}" is read-only.`) return false }, ownKeys() { const plugin = getPlugin() const configuration = buildConfiguration(plugin) return [...Reflect.ownKeys(plugin), ...Reflect.ownKeys(configuration)] }, getOwnPropertyDescriptor() { return { enumerable: true, configurable: true, } }, }) return proxy } const isPluginUnavailable = ( cache: undefined | { plugin: Plugin; code: string }, ): cache is undefined => { return ( !cache || !cache.plugin || cache.plugin.disabled || (cache.plugin.install && !cache.plugin.installed) ) } const reloadPlugin = async (plugin: Plugin, code = '', reloadTrigger = false) => { const { path } = plugin if (!code) { code = await ReadFile(path) } PluginsCache[plugin.id] = { plugin, code } reloadTrigger && updatePluginTrigger(plugin) } // FIXME: Plug-in execution order is wrong const updatePluginTrigger = (plugin: Plugin, isUpdate = true) => { const triggers = Object.keys(PluginsTriggerMap) as PluginTrigger[] triggers.forEach((trigger) => { PluginsTriggerMap[trigger].observers = PluginsTriggerMap[trigger].observers.filter( (v) => v !== plugin.id, ) }) if (isUpdate) { plugin.triggers.forEach((trigger) => { PluginsTriggerMap[trigger].observers.push(plugin.id) }) } } const savePlugins = () => { const p = omitArray(plugins.value, ['updating', 'loading', 'running']) return WriteFile(PluginsFilePath, stringifyNoFolding(p)) } const addPlugin = async (plugin: Plugin) => { plugins.value.push(plugin) try { await _doUpdatePlugin(plugin) await savePlugins() updatePluginTrigger(plugin) } catch (error) { const idx = plugins.value.indexOf(plugin) if (idx !== -1) { plugins.value.splice(idx, 1) } throw error } } const deletePlugin = async (id: string) => { const idx = plugins.value.findIndex((v) => v.id === id) if (idx === -1) return const plugin = plugins.value.splice(idx, 1)[0]! try { await savePlugins() delete PluginsCache[id] updatePluginTrigger(plugin, false) } catch (error) { plugins.value.splice(idx, 0, plugin) throw error } plugin.path.startsWith('data') && (await RemoveFile(plugin.path).catch((_) => {})) // Remove configuration if (appSettingsStore.app.pluginSettings[plugin.id]) { if (await confirm('Tips', 'plugins.removeConfiguration').catch(() => 0)) { delete appSettingsStore.app.pluginSettings[plugin.id] } } } const editPlugin = async (id: string, newPlugin: Plugin) => { const idx = plugins.value.findIndex((v) => v.id === id) if (idx === -1) return const plugin = plugins.value.splice(idx, 1, newPlugin)[0]! try { await savePlugins() if (PluginsCache[plugin.id]) { PluginsCache[plugin.id]!.plugin = newPlugin } updatePluginTrigger(newPlugin) } catch (error) { plugins.value.splice(idx, 1, plugin) throw error } } const _doUpdatePlugin = async (plugin: Plugin) => { const isFromPluginHub = plugin.id.startsWith('plugin-') if (isFromPluginHub) { const newPlugin = pluginHub.value.find((v) => v.id === plugin.id) if (!newPlugin) throw 'Plugin not found. Please update the Plugin-Hub.' const [major_now, minor_now, patch_now] = (plugin.version || '').substring(1).split('.') const [major_new, minor_new, patch_new] = (newPlugin.version || '').substring(1).split('.') if (major_now !== major_new) { await editPlugin(plugin.id, deepClone(newPlugin)) const userSettigns = appSettingsStore.app.pluginSettings[plugin.id] if (userSettigns) { appSettingsStore.app.pluginSettings[plugin.id] = newPlugin.configuration.reduce( (p, c) => { const value_now = userSettigns[c.key] const value_new = c.value const type_now = Array.isArray(value_now) ? 'array' : typeof value_now const type_new = Array.isArray(value_new) ? 'array' : typeof value_new return { ...p, [c.key]: type_now === type_new ? value_now : value_new, } }, {}, ) } } else if (minor_now !== minor_new || patch_now !== patch_new) { plugin.version = newPlugin.version await editPlugin(plugin.id, plugin) } } let code = '' if (plugin.type === 'File') { code = await ReadFile(plugin.path).catch(() => '') } if (plugin.type === 'Http') { const { body } = await HttpGet(plugin.url) code = body } if (plugin.type !== 'File') { await WriteFile(plugin.path, code) } PluginsCache[plugin.id] = { plugin, code } } const updatePlugin = async (id: string) => { const plugin = plugins.value.find((v) => v.id === id) if (!plugin) throw id + ' Not Found' if (plugin.disabled) throw plugin.name + ' is Disabled' try { plugin.updating = true await _doUpdatePlugin(plugin) return `Plugin [${plugin.name}] updated successfully.` } finally { plugin.updating = false } } const updatePlugins = async () => { let needSave = false const update = async (plugin: Plugin) => { const result = { ok: true, id: plugin.id, name: plugin.name, result: '' } try { plugin.updating = true await _doUpdatePlugin(plugin) needSave = true result.result = `Plugin [${plugin.name}] updated successfully.` } catch (error: any) { result.ok = false result.result = `Failed to update plugin [${plugin.name}]. Reason: ${error.message || error}` } finally { plugin.updating = false } return result } const result = await asyncPool( 5, plugins.value.filter((v) => !v.disabled), update, ) if (needSave) await savePlugins() return result.flatMap((v) => (v.ok && v.value) || []) } const pluginHubLoading = ref(false) const findPluginInHubById = (id: string) => pluginHub.value.find((v) => v.id === id) const isDeprecated = (plugin: Plugin) => { if (!plugin.id.startsWith('plugin-')) return false return !findPluginInHubById(plugin.id) } const isDevVersion = (plugin: Plugin) => { return plugin.version.startsWith('v0') } const hasNewPluginVersion = (plugin: Plugin) => { const p = findPluginInHubById(plugin.id) if (!p) return false return p.version !== plugin.version } const updatePluginHub = async () => { pluginHubLoading.value = true try { const { body: body1 } = await HttpGet( 'https://raw.githubusercontent.com/GUI-for-Cores/Plugin-Hub/main/plugins/generic.json', ) const { body: body2 } = await HttpGet( 'https://raw.githubusercontent.com/GUI-for-Cores/Plugin-Hub/main/plugins/gfs.json', ) pluginHub.value = [...JSON.parse(body1), ...JSON.parse(body2)] await WriteFile(PluginHubFilePath, JSON.stringify(pluginHub.value)) } finally { pluginHubLoading.value = false } } const getPluginById = (id: string) => plugins.value.find((v) => v.id === id) const getPluginCodefromCache = (id: string) => PluginsCache[id]?.code const onSubscribeTrigger = async (proxies: Recordable[], subscription: Subscription) => { const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnSubscribe] if (observers.length === 0) return proxies subscription = deepClone(subscription) for (const observer of observers) { const cache = PluginsCache[observer] if (isPluginUnavailable(cache)) continue const metadata = getPluginMetadata(observer) try { const fn = new window.AsyncFunction( 'Plugin', 'proxies', 'subscription', `${cache.code}; return await ${fnName}(proxies, subscription)`, ) proxies = await fn(metadata, proxies, subscription) } catch (error: any) { throw `${cache.plugin.name} : ` + (error.message || error) } if (!Array.isArray(proxies)) { throw `${cache.plugin.name} : Wrong result` } } return proxies } const noParamsTrigger = async (trigger: PluginTrigger, interruptOnError = false) => { const { fnName, observers } = PluginsTriggerMap[trigger] if (observers.length === 0) return for (const observer of observers) { const cache = PluginsCache[observer] if (isPluginUnavailable(cache)) continue const metadata = getPluginMetadata(observer) try { const fn = new window.AsyncFunction('Plugin', `${cache.code}; return await ${fnName}()`) const exitCode = await fn(metadata) if (isNumber(exitCode) && exitCode !== cache.plugin.status) { cache.plugin.status = exitCode editPlugin(cache.plugin.id, cache.plugin) } } catch (error: any) { const msg = `${cache.plugin.name} : ` + (error.message || error) if (interruptOnError) { throw msg } console.error(msg) } } } const onGenerateTrigger = async (config: Recordable, profile: IProfile) => { const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnGenerate] if (observers.length === 0) return config profile = deepClone(profile) for (const observer of observers) { const cache = PluginsCache[observer] if (isPluginUnavailable(cache)) continue const metadata = getPluginMetadata(observer) try { const fn = new window.AsyncFunction( 'Plugin', 'config', 'profile', `${cache.code}; return await ${fnName}(config, profile)`, ) config = await fn(metadata, config, profile) } catch (error: any) { throw `${cache.plugin.name} : ` + (error.message || error) } if (!config) throw `${cache.plugin.name} : Wrong result` } return config } const onBeforeCoreStartTrigger = async (params: Recordable, profile: IProfile) => { const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnBeforeCoreStart] if (observers.length === 0) return params profile = deepClone(profile) for (const observer of observers) { const cache = PluginsCache[observer] if (isPluginUnavailable(cache)) continue const metadata = getPluginMetadata(observer) try { const fn = new window.AsyncFunction( 'Plugin', 'config', 'profile', `${cache.code}; return await ${fnName}(config, profile)`, ) params = await fn(metadata, params, profile) } catch (error: any) { throw `${cache.plugin.name} : ` + (error.message || error) } if (!params) throw `${cache.plugin.name} : Wrong result` } return params } const manualTrigger = async (id: string, event: PluginTriggerEvent, ...args: any[]) => { const plugin = getPluginById(id) if (!plugin) throw id + ' Not Found' const cache = PluginsCache[plugin.id] if (!cache) throw `${plugin.name} is Missing source code` if (cache.plugin.disabled) throw `${plugin.name} is Disabled` const metadata = getPluginMetadata(id) args = deepClone(args) try { const fn = new window.AsyncFunction( 'Plugin', '...args', `${cache.code}; return await ${event}(...args)`, ) const exitCode = await fn(metadata, ...args) if (isNumber(exitCode) && exitCode !== plugin.status) { plugin.status = exitCode editPlugin(id, plugin) } return exitCode } catch (error: any) { throw `${cache.plugin.name} : ` + (error.message || error) } } const onTrayUpdateTrigger = async (tray: TrayContent, menus: MenuItem[]) => { const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnTrayUpdate] if (observers.length === 0) return [tray, menus] as const let finalTray = tray let finalMenus = menus for (const observer of observers) { const cache = PluginsCache[observer] if (isPluginUnavailable(cache)) continue const metadata = getPluginMetadata(observer) try { const fn = new window.AsyncFunction( 'Plugin', 'tray', 'menus', `${cache.code}; return await ${fnName}(tray, menus)`, ) const { tray, menus } = await fn(metadata, finalTray, finalMenus) finalTray = tray finalMenus = menus } catch (error: any) { throw `${cache.plugin.name} : ` + (error.message || error) } } return [finalTray, finalMenus] as const } const _watchDisabled = computed(() => plugins.value .map((v) => v.disabled) .sort() .join(), ) const _watchMenus = computed(() => plugins.value .map((v) => Object.entries(v.menus).map((v) => v[0] + v[1])) .sort() .join(), ) watch([_watchMenus, _watchDisabled], () => { if (appSettingsStore.app.addPluginToMenu) { updateTrayAndMenus() } }) return { plugins, setupPlugins, savePlugins, addPlugin, editPlugin, deletePlugin, updatePlugin, updatePlugins, getPluginById, reloadPlugin, onTrayUpdateTrigger, onSubscribeTrigger, onGenerateTrigger, onStartupTrigger: () => noParamsTrigger(PluginTrigger.OnStartup), onShutdownTrigger: () => noParamsTrigger(PluginTrigger.OnShutdown, true), onReadyTrigger: () => noParamsTrigger(PluginTrigger.OnReady), onReloadTrigger: () => noParamsTrigger(PluginTrigger.OnReload, true), onCoreStartedTrigger: () => noParamsTrigger(PluginTrigger.OnCoreStarted), onCoreStoppedTrigger: () => noParamsTrigger(PluginTrigger.OnCoreStopped), onBeforeCoreStopTrigger: () => noParamsTrigger(PluginTrigger.OnBeforeCoreStop, true), onBeforeCoreStartTrigger, manualTrigger, updatePluginTrigger, getPluginCodefromCache, getPluginMetadata, pluginHub, pluginHubLoading, updatePluginHub, hasNewPluginVersion, findPluginInHubById, isDeprecated, isDevVersion, } }) ================================================ FILE: frontend/src/stores/profiles.ts ================================================ import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { parse } from 'yaml' import { ReadFile, WriteFile } from '@/bridge' import { ProfilesFilePath } from '@/constant/app' import * as Defaults from '@/constant/profile' import { useAppSettingsStore } from '@/stores' import { ignoredError, eventBus, stringifyNoFolding, migrateProfiles, sampleID } from '@/utils' export const useProfilesStore = defineStore('profiles', () => { const appSettingsStore = useAppSettingsStore() const profiles = ref([]) const currentProfile = computed(() => getProfileById(appSettingsStore.app.kernel.profile)) const setupProfiles = async () => { const data = await ignoredError(ReadFile, ProfilesFilePath) data && (profiles.value = parse(data)) await migrateProfiles(profiles.value, saveProfiles) } const saveProfiles = () => { return WriteFile(ProfilesFilePath, stringifyNoFolding(profiles.value)) } const addProfile = async (p: IProfile) => { profiles.value.push(p) try { await saveProfiles() } catch (error) { const idx = profiles.value.indexOf(p) if (idx !== -1) { profiles.value.splice(idx, 1) } throw error } } const deleteProfile = async (id: string) => { const idx = profiles.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = profiles.value.splice(idx, 1)[0]! try { await saveProfiles() } catch (error) { profiles.value.splice(idx, 0, backup) throw error } eventBus.emit('profileChange', { id }) } const editProfile = async (id: string, p: IProfile) => { const idx = profiles.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = profiles.value.splice(idx, 1, p)[0]! try { await saveProfiles() } catch (error) { profiles.value.splice(idx, 1, backup) throw error } eventBus.emit('profileChange', { id }) } const getProfileById = (id: string) => profiles.value.find((v) => v.id === id) const getProfileTemplate = (name = ''): IProfile => { return { id: sampleID(), name: name, log: Defaults.DefaultLog(), experimental: Defaults.DefaultExperimental(), inbounds: Defaults.DefaultInbounds(), outbounds: Defaults.DefaultOutbounds(), route: Defaults.DefaultRoute(), dns: Defaults.DefaultDns(), mixin: Defaults.DefaultMixin(), script: Defaults.DefaultScript(), } } return { profiles, currentProfile, setupProfiles, saveProfiles, addProfile, editProfile, deleteProfile, getProfileById, getProfileTemplate, } }) ================================================ FILE: frontend/src/stores/rulesets.ts ================================================ import { defineStore } from 'pinia' import { ref } from 'vue' import { parse } from 'yaml' import { ReadFile, WriteFile, CopyFile, Download, HttpGet } from '@/bridge' import { RulesetHubFilePath, RulesetsFilePath } from '@/constant/app' import { EmptyRuleSet } from '@/constant/kernel' import { RulesetFormat } from '@/enums/kernel' import { asyncPool, stringifyNoFolding, eventBus, ignoredError, isValidRulesJson, omitArray, } from '@/utils' export interface RuleSet { id: string tag: string updateTime: number disabled: boolean type: 'Http' | 'File' | 'Manual' format: RulesetFormat path: string url: string count: number // Not Config updating?: boolean } export interface RuleSetHub { geosite: string geoip: string list: { name: string; type: 'geosite' | 'geoip'; description: string; count: number }[] } export const useRulesetsStore = defineStore('rulesets', () => { const rulesets = ref([]) const rulesetHub = ref({ geosite: '', geoip: '', list: [] }) const setupRulesets = async () => { const data = await ignoredError(ReadFile, RulesetsFilePath) data && (rulesets.value = parse(data)) const list = await ignoredError(ReadFile, RulesetHubFilePath) list && (rulesetHub.value = JSON.parse(list)) } const saveRulesets = () => { const r = omitArray(rulesets.value, ['updating']) return WriteFile(RulesetsFilePath, stringifyNoFolding(r)) } const addRuleset = async (r: RuleSet) => { rulesets.value.push(r) try { await saveRulesets() } catch (error) { const idx = rulesets.value.indexOf(r) if (idx !== -1) { rulesets.value.splice(idx, 1) } throw error } } const deleteRuleset = async (id: string) => { const idx = rulesets.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = rulesets.value.splice(idx, 1)[0]! try { await saveRulesets() } catch (error) { rulesets.value.splice(idx, 0, backup) throw error } eventBus.emit('rulesetChange', { id }) } const editRuleset = async (id: string, r: RuleSet) => { const idx = rulesets.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = rulesets.value.splice(idx, 1, r)[0]! try { await saveRulesets() } catch (error) { rulesets.value.splice(idx, 1, backup) throw error } eventBus.emit('rulesetChange', { id }) } const _doUpdateRuleset = async (r: RuleSet) => { if (r.format === RulesetFormat.Source) { let body = '' let isExist = true if (r.type === 'File') { body = await ReadFile(r.url) } else if (r.type === 'Http') { const { body: b } = await HttpGet(r.url) body = b if (typeof body !== 'string') { body = JSON.stringify(body) } } else if (r.type === 'Manual') { body = await ReadFile(r.path).catch(() => '') if (!body) { body = JSON.stringify(EmptyRuleSet) isExist = false } } if (!isValidRulesJson(body)) { throw 'Not a valid ruleset data' } const ruleset = JSON.parse(body) r.count = ruleset.rules.reduce( (p: number, c: string[]) => Object.values(c).reduce( (p, c: string[] | string) => (Array.isArray(c) ? p + c.length : p + 1), 0, ) + p, 0, ) if ( (['Http', 'File'].includes(r.type) && r.url !== r.path) || (r.type === 'Manual' && !isExist) ) { await WriteFile(r.path, JSON.stringify(ruleset, null, 2)) } } if (r.format === RulesetFormat.Binary) { if (r.type === 'File' && r.url !== r.path) { await CopyFile(r.url, r.path) } else if (r.type === 'Http') { await Download(r.url, r.path) } } r.updateTime = Date.now() } const updateRuleset = async (id: string) => { const r = rulesets.value.find((v) => v.id === id) if (!r) throw id + ' Not Found' if (r.disabled) throw r.tag + ' Disabled' try { r.updating = true await _doUpdateRuleset(r) await saveRulesets() } finally { r.updating = false } eventBus.emit('rulesetChange', { id }) return `Ruleset [${r.tag}] updated successfully.` } const updateRulesets = async () => { let needSave = false const update = async (r: RuleSet) => { const result = { ok: true, id: r.id, name: r.tag, result: '' } try { r.updating = true await _doUpdateRuleset(r) needSave = true result.result = `Rule-Set [${r.tag}] updated successfully.` } catch (error: any) { result.ok = false result.result = `Failed to update rule-set [${r.tag}]. Reason: ${error.message || error}` } finally { r.updating = false } return result } const result = await asyncPool( 5, rulesets.value.filter((v) => !v.disabled), update, ) if (needSave) await saveRulesets() eventBus.emit('rulesetsChange', undefined) return result.flatMap((v) => (v.ok && v.value) || []) } const rulesetHubLoading = ref(false) const updateRulesetHub = async () => { rulesetHubLoading.value = true try { const { body } = await HttpGet( 'https://github.com/GUI-for-Cores/Ruleset-Hub/releases/download/latest/sing-full.json', ) rulesetHub.value = JSON.parse(body) await WriteFile(RulesetHubFilePath, body) } finally { rulesetHubLoading.value = false } } const getRulesetById = (id: string) => rulesets.value.find((v) => v.id === id) return { rulesets, setupRulesets, saveRulesets, addRuleset, editRuleset, deleteRuleset, updateRuleset, updateRulesets, getRulesetById, rulesetHub, rulesetHubLoading, updateRulesetHub, } }) ================================================ FILE: frontend/src/stores/scheduledtasks.ts ================================================ import { Cron } from 'croner' import { defineStore } from 'pinia' import { ref } from 'vue' import { parse } from 'yaml' import { ReadFile, WriteFile, Notify } from '@/bridge' import { ScheduledTasksFilePath } from '@/constant/app' import { ScheduledTasksType, PluginTriggerEvent } from '@/enums/app' import { useSubscribesStore, useRulesetsStore, usePluginsStore, useLogsStore } from '@/stores' import { ignoredError, stringifyNoFolding } from '@/utils' import type { ScheduledTask } from '@/types/app' export const useScheduledTasksStore = defineStore('scheduledtasks', () => { const scheduledtasks = ref([]) const cronJobsMap: Recordable = {} const setupScheduledTasks = async () => { const data = await ignoredError(ReadFile, ScheduledTasksFilePath) data && (scheduledtasks.value = parse(data)) scheduledtasks.value.forEach(async ({ disabled, cron, id }) => { if (!disabled) { cronJobsMap[id] = new Cron(cron, () => runScheduledTask(id)) } }) } const runScheduledTask = async (id: string) => { const task = getScheduledTaskById(id) if (!task) return const logsStore = useLogsStore() task.lastTime = Date.now() const startTime = Date.now() const result = await getTaskFn(task)() if (task.notification) { const successes = result.filter((v) => v.ok).length const failures = result.length - successes const details = result.flatMap((v) => v.result).join('\n') const content = `Successes: ${successes}; Failures: ${failures}. \n\n${details}` Notify(task.name, content) } logsStore.recordScheduledTasksLog({ name: task.name, startTime, endTime: Date.now(), result: result, }) await editScheduledTask(id, task) } const withOutput = (list: string[], fn: (id: string) => Promise) => { return async () => { const output: { ok: boolean; result: T }[] = [] for (const id of list) { try { const result = await fn(id) if (Array.isArray(result)) { output.push(...result) } else { output.push({ ok: true, result }) } } catch (error: any) { output.push({ ok: false, result: error.message || error }) } } return output } } const getTaskFn = (task: ScheduledTask) => { switch (task.type) { case ScheduledTasksType.UpdateSubscription: { const subscribesStore = useSubscribesStore() return withOutput(task.subscriptions, subscribesStore.updateSubscribe) } case ScheduledTasksType.UpdateRuleset: { const rulesetsStore = useRulesetsStore() return withOutput(task.rulesets, rulesetsStore.updateRuleset) } case ScheduledTasksType.UpdatePlugin: { const pluginsStores = usePluginsStore() return withOutput(task.plugins, pluginsStores.updatePlugin) } case ScheduledTasksType.UpdateAllSubscription: { const subscribesStore = useSubscribesStore() return withOutput(['0'], () => subscribesStore.updateSubscribes()) } case ScheduledTasksType.UpdateAllRuleset: { const rulesetsStore = useRulesetsStore() return withOutput(['1'], () => rulesetsStore.updateRulesets()) } case ScheduledTasksType.UpdateAllPlugin: { const pluginsStores = usePluginsStore() return withOutput(['2'], () => pluginsStores.updatePlugins()) } case ScheduledTasksType.RunPlugin: { const pluginsStores = usePluginsStore() return withOutput(task.plugins, async (id: string) => pluginsStores.manualTrigger(id, PluginTriggerEvent.OnTask), ) } case ScheduledTasksType.RunScript: { return withOutput([task.script], (script: string) => new window.AsyncFunction(script)()) } } } const saveScheduledTasks = () => { return WriteFile(ScheduledTasksFilePath, stringifyNoFolding(scheduledtasks.value)) } const addScheduledTask = async (s: ScheduledTask) => { scheduledtasks.value.push(s) try { cronJobsMap[s.id] = new Cron(s.cron, () => runScheduledTask(s.id)) await saveScheduledTasks() } catch (error) { cronJobsMap[s.id]?.stop() delete cronJobsMap[s.id] const idx = scheduledtasks.value.indexOf(s) if (idx !== -1) { scheduledtasks.value.splice(idx, 1) } throw error } } const deleteScheduledTask = async (id: string) => { const idx = scheduledtasks.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = scheduledtasks.value.splice(idx, 1)[0]! try { await saveScheduledTasks() cronJobsMap[id]?.stop() delete cronJobsMap[id] } catch (error) { scheduledtasks.value.splice(idx, 0, backup) throw error } } const editScheduledTask = async (id: string, s: ScheduledTask) => { const idx = scheduledtasks.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = scheduledtasks.value.splice(idx, 1, s)[0]! try { await saveScheduledTasks() cronJobsMap[id]?.stop() if (s.disabled) { delete cronJobsMap[id] } else { cronJobsMap[id] = new Cron(s.cron, () => runScheduledTask(id)) } } catch (error) { scheduledtasks.value.splice(idx, 1, backup) throw error } } const getScheduledTaskById = (id: string) => scheduledtasks.value.find((v) => v.id === id) return { scheduledtasks, setupScheduledTasks, saveScheduledTasks, addScheduledTask, editScheduledTask, deleteScheduledTask, getScheduledTaskById, getTaskFn, runScheduledTask, } }) ================================================ FILE: frontend/src/stores/subscribes.ts ================================================ import { defineStore } from 'pinia' import { ref } from 'vue' import { parse } from 'yaml' import { ReadFile, WriteFile, Requests } from '@/bridge' import { DefaultSubscribeScript, SubscribesFilePath } from '@/constant/app' import { DefaultExcludeProtocols } from '@/constant/kernel' import { PluginTriggerEvent, RequestMethod } from '@/enums/app' import { usePluginsStore } from '@/stores' import { sampleID, isValidSubJson, isValidSubYAML, isValidBase64, stringifyNoFolding, ignoredError, omitArray, asyncPool, eventBus, buildSmartRegExp, } from '@/utils' import type { Subscription } from '@/types/app' export const useSubscribesStore = defineStore('subscribes', () => { const subscribes = ref([]) const setupSubscribes = async () => { const data = await ignoredError(ReadFile, SubscribesFilePath) data && (subscribes.value = parse(data)) } const saveSubscribes = () => { const s = omitArray(subscribes.value, ['updating']) return WriteFile(SubscribesFilePath, stringifyNoFolding(s)) } const addSubscribe = async (s: Subscription) => { subscribes.value.push(s) try { await saveSubscribes() } catch (error) { const idx = subscribes.value.indexOf(s) if (idx !== -1) { subscribes.value.splice(idx, 1) } throw error } } const importSubscribe = async (name: string, url: string) => { await addSubscribe(getSubscribeTemplate(name, { url })) } const deleteSubscribe = async (id: string) => { const idx = subscribes.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = subscribes.value.splice(idx, 1)[0]! try { await saveSubscribes() } catch (error) { subscribes.value.splice(idx, 0, backup) throw error } eventBus.emit('subscriptionChange', { id }) } const editSubscribe = async (id: string, s: Subscription) => { const idx = subscribes.value.findIndex((v) => v.id === id) if (idx === -1) return const backup = subscribes.value.splice(idx, 1, s)[0]! try { await saveSubscribes() } catch (error) { subscribes.value.splice(idx, 1, backup) throw error } eventBus.emit('subscriptionChange', { id }) } const _doUpdateSub = async (s: Subscription) => { const userInfo: Recordable = {} let body = '' let proxies: Record[] = [] if (s.type === 'Manual') { body = await ReadFile(s.path) } if (s.type === 'File') { body = await ReadFile(s.url) } if (s.type === 'Http') { const { headers: h, body: b } = await Requests({ method: s.requestMethod, url: s.url, headers: s.header.request, autoTransformBody: false, options: { Insecure: s.inSecure, Timeout: s.requestTimeout, }, }) Object.assign(h, s.header.response) if (h['Subscription-Userinfo']) { ;(h['Subscription-Userinfo'] as string).split(/\s*;\s*/).forEach((part) => { const [key, value] = part.split('=') as [string, string] userInfo[key] = parseInt(value) || 0 }) } body = b } if (isValidSubJson(body)) { proxies = JSON.parse(body).outbounds } else if (isValidSubYAML(body)) { proxies = parse(body).proxies } else if (isValidBase64(body)) { proxies = [{ base64: body }] } else if (s.type === 'Manual') { proxies = JSON.parse(body) } else { throw 'Not a valid subscription data' } const pluginStore = usePluginsStore() proxies = await pluginStore.onSubscribeTrigger(proxies, s) if (proxies.some((proxy) => proxy.name && !proxy.tag) || proxies[0]?.base64) { throw 'You need to install the [节点转换] plugin first' } if (s.type !== 'Manual') { const r1 = s.include && buildSmartRegExp(s.include) const r2 = s.exclude && buildSmartRegExp(s.exclude) const r3 = s.includeProtocol && buildSmartRegExp(s.includeProtocol) const r4 = s.excludeProtocol && buildSmartRegExp(s.excludeProtocol) proxies = proxies.filter((v) => { const flag1 = r1 ? r1.test(v.tag) : true const flag2 = r2 ? r2.test(v.tag) : false const flag3 = r3 ? r3.test(v.type) : true const flag4 = r4 ? r4.test(v.type) : false return flag1 && !flag2 && flag3 && !flag4 }) if (s.proxyPrefix) { proxies.forEach((v) => { v.tag = v.tag.startsWith(s.proxyPrefix) ? v.tag : s.proxyPrefix + v.tag }) } } s.upload = userInfo.upload ?? 0 s.download = userInfo.download ?? 0 s.total = userInfo.total ?? 0 s.expire = userInfo.expire * 1000 s.updateTime = Date.now() s.proxies = proxies.map(({ tag, type }) => { // Keep the original ID value of the proxy unchanged const id = s.proxies.find((v) => v.tag === tag)?.id || sampleID() return { id, tag, type } }) const fn = new window.AsyncFunction( 'proxies', 'subscription', `${s.script}; return await ${PluginTriggerEvent.OnSubscribe}(proxies, subscription)`, ) as ( proxies: Recordable[], subscription: Subscription, ) => Promise<{ proxies: Recordable[]; subscription: Subscription }> const { proxies: _proxies, subscription } = await fn(proxies, s) Object.assign(s, subscription) s.proxies = _proxies.map(({ tag, type }) => { // Keep the original ID value of the proxy unchanged const id = s.proxies.find((v) => v.tag === tag)?.id || sampleID() return { id, tag, type } }) if (s.type === 'Http' || (s.type === 'File' && s.url !== s.path)) { proxies = omitArray(_proxies, ['__id__', '__tmp__id__']) await WriteFile(s.path, JSON.stringify(proxies, null, 2)) } } const updateSubscribe = async (id: string) => { const s = subscribes.value.find((v) => v.id === id) if (!s) throw id + ' Not Found' if (s.disabled) throw s.name + ' Disabled' try { s.updating = true await _doUpdateSub(s) await saveSubscribes() } catch (error: any) { throw `Failed to update subscription [${s.name}]. Reason: ${error.message || error}` } finally { s.updating = false } eventBus.emit('subscriptionChange', { id }) return `Subscription [${s.name}] updated successfully.` } const updateSubscribes = async () => { let needSave = false const update = async (s: Subscription) => { const result = { ok: true, id: s.id, name: s.name, result: '' } try { s.updating = true await _doUpdateSub(s) needSave = true result.result = `Subscription [${s.name}] updated successfully.` } catch (error: any) { result.ok = false result.result = `Failed to update subscription [${s.name}]. Reason: ${error.message || error}` } finally { s.updating = false } return result } const result = await asyncPool( 5, subscribes.value.filter((v) => !v.disabled), update, ) if (needSave) await saveSubscribes() eventBus.emit('subscriptionsChange', undefined) return result.flatMap((v) => (v.ok && v.value) || []) } const getSubscribeById = (id: string) => subscribes.value.find((v) => v.id === id) const getSubscribeTemplate = (name = '', options: { url?: string } = {}): Subscription => { const id = sampleID() return { id: id, name: name, upload: 0, download: 0, total: 0, expire: 0, updateTime: 0, type: 'Http', url: options.url || '', website: '', path: `data/subscribes/${id}.json`, include: '', exclude: '', includeProtocol: '', excludeProtocol: DefaultExcludeProtocols, proxyPrefix: '', disabled: false, inSecure: false, requestMethod: RequestMethod.Get, requestTimeout: 15, header: { request: {}, response: {}, }, proxies: [], script: DefaultSubscribeScript, } } return { subscribes, setupSubscribes, saveSubscribes, addSubscribe, editSubscribe, deleteSubscribe, updateSubscribe, updateSubscribes, getSubscribeById, importSubscribe, getSubscribeTemplate, } }) ================================================ FILE: frontend/src/types/app.d.ts ================================================ import { h, ref, type VNode } from 'vue' import type { Lang, Theme, Color, View, WindowStartState, WebviewGpuPolicy, Branch, ControllerCloseMode, PluginTrigger, ScheduledTasksType, RequestMethod, } from '@/enums/app' export interface TrayContent { icon?: string title?: string tooltip?: string } export interface Menu { label: string handler?: (...args: any) => void separator?: boolean children?: Menu[] } export interface MenuItem { type: 'item' | 'separator' text?: string tooltip?: string event?: (() => void) | string children?: MenuItem[] hidden?: boolean checked?: boolean } export interface AppSettings { lang: Lang | string theme: Theme color: Color primaryColor: string secondaryColor: string fontFamily: string profilesView: View subscribesView: View rulesetsView: View pluginsView: View scheduledtasksView: View windowStartState: WindowStartState webviewGpuPolicy: WebviewGpuPolicy width: number height: number exitOnClose: boolean closeKernelOnExit: boolean autoSetSystemProxy: boolean proxyBypassList: string autoStartKernel: boolean autoRestartKernel: boolean userAgent: string startupDelay: number connections: { visibility: Record order: string[] } kernel: { realMemoryUsage: boolean branch: Branch profile: string autoClose: boolean unAvailable: boolean cardMode: boolean cardColumns: number sortByDelay: boolean testUrl: string testTimeout: number concurrencyLimit: number controllerCloseMode: ControllerCloseMode controllerSensitivity: number main: { env: Recordable args: string[] } alpha: { env: Recordable args: string[] } } pluginSettings: Record> githubApiToken: string multipleInstance: boolean addPluginToMenu: boolean addGroupToMenu: boolean rollingRelease: boolean debugOutline: boolean debugNoAnimation: boolean debugNoRounded: false debugBorder: boolean pages: string[] } export interface PluginConfiguration { id: string title: string description: string key: string component: | 'CheckBox' | 'CodeViewer' | 'Input' | 'InputList' | 'KeyValueEditor' | 'Radio' | 'Select' | 'MultipleSelect' | 'Switch' | 'ColorPicker' | '' value: any options: any[] } export interface Plugin { id: string version: string name: string description: string type: 'Http' | 'File' url: string path: string triggers: PluginTrigger[] tags: string[] hasUI: boolean menus: Record context: { profiles: Recordable subscriptions: Recordable rulesets: Recordable plugins: Recordable scheduledtasks: Recordable } configuration: PluginConfiguration[] disabled: boolean install: boolean installed: boolean status: number // 0: Normal 1: Running 2: Stopped // Not Config updating?: boolean loading?: boolean running?: boolean } export interface ScheduledTask { id: string name: string type: ScheduledTasksType subscriptions: string[] rulesets: string[] plugins: string[] script: string cron: string notification: boolean disabled: boolean lastTime: number } export interface Subscription { id: string name: string upload: number download: number total: number expire: number updateTime: number type: 'Http' | 'File' | 'Manual' url: string website: string path: string include: string exclude: string includeProtocol: string excludeProtocol: string proxyPrefix: string disabled: boolean inSecure: boolean proxies: { id: string; tag: string; type: string }[] requestMethod: RequestMethod requestTimeout: number header: { request: Recordable response: Recordable } script: string // Not Config updating?: boolean } // Custom Action export interface CustomActionApi { h: typeof h ref: typeof ref } type CustomActionProps = Recordable type CustomActionSlots = Recordable< ((api: CustomActionApi) => VNode | string | number | boolean) | VNode | string | number | boolean > export interface CustomAction

{ id?: string component: string componentProps?: P | ((api: CustomActionApi) => P) componentSlots?: S | ((api: CustomActionApi) => S) } export type CustomActionFn = ((api: CustomActionApi) => CustomAction) & { id?: string } ================================================ FILE: frontend/src/types/global.d.ts ================================================ interface Window { /** * The variable is initialized in `globalMethods.ts:11` */ Plugins: any /** * The variable is initialized in `globalMethods.ts:23` */ AsyncFunction: FunctionConstructor /** * The variable is initialized in `globalMethods.ts:21` */ Vue: any /** * The variable is initialized in `main.ts:15` */ appInstance: any } ================================================ FILE: frontend/src/types/kernel.d.ts ================================================ export interface CoreApiConfig { port: number 'socks-port': number 'mixed-port': number 'interface-name': string 'allow-lan': boolean mode: string tun: { enable: boolean stack: string device: string } } export interface CoreApiProxy { alive: boolean all: string[] name: string now: string type: string udp: boolean history: { delay: number }[] } export interface CoreApiProxies { proxies: Record } export interface CoreApiConnections { connections: { id: string chains: string[] }[] } export interface CoreApiTrafficData { down: number up: number } export interface CoreApiMemoryData { inuse: number oslimit: number } export interface CoreApiLogsData { type: string payload: string } export interface CoreApiConnectionsData { memory: number uploadTotal: number downloadTotal: number connections: { chains: string[] download: number id: string metadata: { destinationIP: string destinationPort: string dnsMode: string host: string network: string processPath: string sourceIP: string sourcePort: string type: string } rule: string rulePayload: string start: string upload: number }[] } export type CoreApiWsDataMap = { logs: CoreApiLogsData memory: CoreApiMemoryData traffic: CoreApiTrafficData connections: CoreApiConnectionsData } ================================================ FILE: frontend/src/types/profile.d.ts ================================================ type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'panic' interface ILog { disabled: boolean level: LogLevel output: string timestamp: boolean } interface IExperimental { clash_api: { external_controller: string external_ui: string external_ui_download_url: string external_ui_download_detour: string secret: string default_mode: string access_control_allow_origin: string[] access_control_allow_private_network: boolean } cache_file: { enabled: boolean path: string cache_id: string store_fakeip: boolean store_rdrc: boolean rdrc_timeout: string } } interface IProxy { id: string type: string tag: string } type RuleSetType = 'inline' | 'local' | 'remote' type RuleSetFormat = 'source' | 'binary' interface IRuleSet { id: string type: RuleSetType tag: string // inline rules: string // local path: string // remote url: string download_detour: string update_interval: string // local or remote format: RuleSetFormat } type InboundType = 'mixed' | 'socks' | 'http' | 'tun' type InboundListen = { listen: string listen_port: number tcp_fast_open: boolean tcp_multi_path: boolean udp_fragment: boolean } interface IInbound { id: string type: InboundType tag: string enable: boolean mixed?: { listen: InboundListen users: string[] } socks?: { listen: InboundListen users: string[] } http?: { listen: InboundListen users: string[] } tun?: { interface_name: string address: string[] mtu: number auto_route: boolean strict_route: boolean route_address: string[] route_exclude_address: string[] endpoint_independent_nat: boolean stack: TunStackEnum } } type OutboundType = 'direct' | 'block' | 'selector' | 'urltest' type RuleAction = 'route' | 'route-options' | 'reject' | 'hijack-dns' | 'sniff' | 'resolve' type DnsRuleAction = 'route' | 'route-options' | 'reject' | 'predefined' interface IOutbound { id: string tag: string type: OutboundType outbounds: IProxy[] url: string interval: string tolerance: number interrupt_exist_connections: boolean // gui include: string exclude: string } type RuleType = | 'inbound' | 'network' | 'protocol' | 'domain' | 'domain_suffix' | 'domain_keyword' | 'domain_regex' | 'source_ip_cidr' | 'ip_cidr' | 'source_port' | 'source_port_range' | 'port' | 'port_range' | 'process_name' | 'process_path' | 'process_path_regex' | 'rule_set' | 'ip_is_private' | 'clash_mode' | 'outbound' | 'inline' | 'InsertionPoint' interface IRule { id: string type: RuleType enable: boolean payload: string invert: boolean action: RuleAction // action = route outbound: string // action = sniff sniffer: string[] // action = resolve strategy: Strategy server: string } interface IRoute { rules: IRule[] rule_set: IRuleSet[] final: string auto_detect_interface: boolean default_interface: string find_process: boolean default_domain_resolver: { server: string client_subnet: string } } type Strategy = 'default' | 'prefer_ipv4' | 'prefer_ipv6' | 'ipv4_only' | 'ipv6_only' type DNSServer = | 'local' | 'hosts' | 'tcp' | 'udp' | 'tls' | 'quic' | 'https' | 'h3' | 'dhcp' | 'fakeip' | 'tailscale' interface IDNSServer { id: string tag: string type: DNSServer // [local,tcp,udp,tls,quic,https/h3,dhcp] detour: string domain_resolver: string // hosts hosts_path: string[] predefined: Recordable // [tcp,udp,tls,quic/https,h3] server: string server_port: string // [https,h3] path: string // dhcp interface: string // fakeip inet4_range: string inet6_range: string } interface IDNSRule { id: string type: RuleType enable: boolean payload: string action: DnsRuleAction invert: boolean // route server: string strategy: Strategy // route/route-options disable_cache: boolean client_subnet: string } interface IDNS { servers: IDNSServer[] rules: IDNSRule[] disable_cache: boolean disable_expire: boolean independent_cache: boolean client_subnet: string final: string strategy: Strategy } type MixinPriority = 'mixin' | 'gui' interface IMixin { priority: MixinPriority format: 'json' | 'yaml' config: string } interface IScript { code: string } interface IProfile { id: string name: string log: ILog experimental: IExperimental inbounds: IInbound[] outbounds: IOutbound[] route: IRoute dns: IDNS mixin: IMixin script: IScript } ================================================ FILE: frontend/src/types/typescript.d.ts ================================================ type Recordable = { [x: string]: T } type MaybePromise = T | Promise ================================================ FILE: frontend/src/utils/command.ts ================================================ import { RestartApp } from '@/bridge' import { ColorOptions, ThemeOptions } from '@/constant/app' import { ModeOptions } from '@/constant/kernel' import { PluginTrigger, PluginTriggerEvent } from '@/enums/app' import useI18n from '@/lang' import { useAppSettingsStore, useAppStore, useEnvStore, useKernelApiStore, usePluginsStore, useRulesetsStore, useSubscribesStore, } from '@/stores' import { exitApp, handleChangeMode, message, reloadApp } from '@/utils' type Command = { label: string cmd: string desc?: string handler?: () => Promise | any children?: Command[] } const processCommands = (commands: Command[], parentLabel = '', parentCmd = '') => { const { t } = useI18n.global const result: Command[] = [] commands.forEach((item) => { const label = parentLabel ? `${t(parentLabel)}: ${t(item.label)}` : t(item.label) const cmd = parentCmd ? `${parentCmd}: ${item.cmd}` : item.cmd if (item.children) { result.push(...processCommands(item.children, label, cmd)) } else { result.push({ label, cmd, handler: item.handler }) } }) return result } export const getCommands = () => { const kernelStore = useKernelApiStore() const appSettings = useAppSettingsStore() const envStore = useEnvStore() const appStore = useAppStore() const subscriptionsStore = useSubscribesStore() const rulesetsStore = useRulesetsStore() const pluginsStore = usePluginsStore() const rawCommands: Command[] = [ { label: 'tray.kernel', cmd: 'Core', children: [ { label: 'tray.startKernel', cmd: 'Start Core', handler: kernelStore.startCore, }, { label: 'tray.stopKernel', cmd: 'Stop Core', handler: kernelStore.stopCore, }, { label: 'tray.restartKernel', cmd: 'Restart Core', handler: kernelStore.restartCore, }, { label: 'tray.enableTunMode', cmd: 'Enable Tun', handler: () => kernelStore.updateConfig('tun', { enable: true }), }, { label: 'tray.disableTunMode', cmd: 'Disable Tun', handler: () => kernelStore.updateConfig('tun', { enable: false }), }, { label: 'kernel.allow-lan', cmd: 'Allow Lan', handler: () => kernelStore.updateConfig('allow-lan', true), }, { label: 'kernel.disallow-lan', cmd: 'Disallow Lan', handler: () => kernelStore.updateConfig('allow-lan', false), }, { label: 'kernel.mode', cmd: 'Core Mode', children: ModeOptions.map((mode) => ({ label: mode.label, cmd: mode.value, handler: () => handleChangeMode(mode.value), })), }, ], }, { label: 'tray.proxy', cmd: 'System Proxy', children: [ { label: 'tray.setSystemProxy', cmd: 'Set System Proxy', handler: envStore.setSystemProxy, }, { label: 'tray.clearSystemProxy', cmd: 'Clear System Proxy', handler: envStore.clearSystemProxy, }, ], }, { label: 'APP', cmd: 'APP', children: [ { label: 'settings.lang.name', cmd: 'Language', children: [ { label: 'settings.lang.load', cmd: 'Load language files', handler: async () => { await appStore.loadLocales() message.success('common.success') }, }, ...appStore.locales.map((v) => ({ label: v.label, cmd: v.value, handler: () => (appSettings.app.lang = v.value), })), ], }, { label: 'settings.theme.name', cmd: 'Theme', children: ThemeOptions.map((theme) => ({ label: theme.label, cmd: theme.value, handler: () => (appSettings.app.theme = theme.value), })), }, { label: 'settings.color.name', cmd: 'Color', children: ColorOptions.map((color) => ({ label: color.label, cmd: color.value, handler: () => (appSettings.app.color = color.value), })), }, { label: 'titlebar.reload', cmd: 'Reload Window', handler: reloadApp, }, { label: 'tray.restartTip', cmd: 'Restart APP', handler: RestartApp, }, { label: 'tray.exitTip', cmd: 'Exit APP', handler: exitApp, }, { label: 'router.about', cmd: 'About APP', handler: () => (appStore.showAbout = true), }, ], }, { label: 'router.subscriptions', cmd: 'Subscriptions', children: [ { label: 'common.updateAll', cmd: 'Update Subscriptions', handler: subscriptionsStore.updateSubscribes, }, ], }, { label: 'router.rulesets', cmd: 'Rulesets', children: [ { label: 'common.updateAll', cmd: 'Update Rulesets', handler: rulesetsStore.updateRulesets, }, ], }, { label: 'router.plugins', cmd: 'Plugins', children: [ { label: 'common.updateAll', cmd: 'Update Plugins', handler: pluginsStore.updatePlugins, }, ], }, { label: 'tray.plugins', cmd: 'Plugins', children: pluginsStore.plugins.flatMap((plugin) => { const hasTrigger = !!plugin.triggers.find((trigger) => trigger === PluginTrigger.OnManual) const hasMenus = !!Object.keys(plugin.menus).length if (!hasTrigger && !hasMenus) return [] const children: Command[] = [] if (hasTrigger) { children.push({ label: 'common.run', cmd: PluginTrigger.OnManual, handler: async () => { plugin.running = true try { await pluginsStore.manualTrigger(plugin.id, PluginTriggerEvent.OnManual) } catch (error: any) { message.error(error) } plugin.running = false }, }) } if (hasMenus) { Object.entries(plugin.menus).forEach(([title, fnName]) => { children.push({ label: title, cmd: fnName, handler: async () => { try { plugin.running = true await pluginsStore.manualTrigger(plugin.id, fnName as any) } catch (error: any) { message.error(error.message || error) } finally { plugin.running = false } }, }) }) } return { label: plugin.name, cmd: plugin.id, children } }), }, ] return processCommands(rawCommands) } ================================================ FILE: frontend/src/utils/completion.ts ================================================ import { snippetCompletion, completeFromList } from '@codemirror/autocomplete' import { scopeCompletionSource, localCompletionSource, snippets } from '@codemirror/lang-javascript' import { PluginTriggerEvent } from '@/enums/app' import i18n from '@/lang' import type { CompletionContext, Completion } from '@codemirror/autocomplete' export const getCompletions = (pluginScope: any = undefined) => { const { t } = i18n.global const snippetsCompletions: Completion[] = [ /** * Built-In */ ...snippets, /** * Plugin Triggers */ snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('common.install')} */\n` + `const ${PluginTriggerEvent.OnInstall} = async () => {\n\t\${}\n\treturn 0\n}`, { label: PluginTriggerEvent.OnInstall, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('common.install'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('common.uninstall')} */\n` + `const ${PluginTriggerEvent.OnUninstall} = async () => {\n\t\${}\n\treturn 0\n}`, { label: PluginTriggerEvent.OnUninstall, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('common.uninstall'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::manual')} */\n` + `const ${PluginTriggerEvent.OnManual} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnManual, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::manual'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::tray::update')} */\n` + `const ${PluginTriggerEvent.OnTrayUpdate} = async (tray, menus) => {\n\t\${}\n\treturn { tray, menus }\n}`, { label: PluginTriggerEvent.OnTrayUpdate, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::tray::update'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::subscribe')} */\n` + `const ${PluginTriggerEvent.OnSubscribe} = async (proxies, subscription) => {\n\t\${}\n\treturn proxies\n}`, { label: PluginTriggerEvent.OnSubscribe, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::subscribe'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::generate')} */\n` + `const ${PluginTriggerEvent.OnGenerate} = async (config, profile) => {\n\t\${}\n\treturn config\n}`, { label: PluginTriggerEvent.OnGenerate, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::generate'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::startup')} */\n` + `const ${PluginTriggerEvent.OnStartup} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnStartup, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::startup'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::shutdown')} */\n` + `const ${PluginTriggerEvent.OnShutdown} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnShutdown, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::shutdown'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::core::started')} */\n` + `const ${PluginTriggerEvent.OnCoreStarted} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnCoreStarted, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::core::started'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::core::stopped')} */\n` + `const ${PluginTriggerEvent.OnCoreStopped} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnCoreStopped, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::core::stopped'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::before::core::start')} */\n` + `const ${PluginTriggerEvent.OnBeforeCoreStart} = async (config, profile) => {\n\t\${}\n\treturn config\n}`, { label: PluginTriggerEvent.OnBeforeCoreStart, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::before::core::start'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::before::core::stop')} */\n` + `const ${PluginTriggerEvent.OnBeforeCoreStop} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnBeforeCoreStop, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::before::core::stop'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::ready')} */\n` + `const ${PluginTriggerEvent.OnReady} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnReady, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::ready'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::reload')} */\n` + `const ${PluginTriggerEvent.OnReload} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnReload, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::reload'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::task')} */\n` + `const ${PluginTriggerEvent.OnTask} = async () => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnTask, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::task'), }, ), snippetCompletion( `/* ${t('plugin.trigger') + ' ' + t('plugin.on::configure')} */\n` + `const ${PluginTriggerEvent.OnConfigure} = async (config, old) => {\n\t\${}\n}`, { label: PluginTriggerEvent.OnConfigure, type: 'keyword', detail: t('plugin.trigger') + ' ' + t('plugin.on::configure'), }, ), /** * Others */ snippetCompletion('console.log(`[$\\{Plugin.name\\}]`, ${})', { label: 'log', type: 'keyword', }), snippetCompletion( "const { close } = await Plugins.StartServer('${address}', '${serverID}', async (req, res) => {\n\tres.end(200, {'Content-Type': 'application/json'}, 'Server is running...')\n})", { label: 'StartServer', type: 'keyword', }, ), snippetCompletion( "await Plugins.Download('${url}', '${path}', {${headers}}, (progress, total) => {\n\t${}\n})", { label: 'Download', type: 'keyword', }, ), snippetCompletion( "await Plugins.Upload('${url}', '${path}', {${headers}}, (progress, total) => {\n\t${}\n})", { label: 'Upload', type: 'keyword', }, ), snippetCompletion( "const { status, headers, body } = await Plugins.Requests({\n\turl: '${url}', \n\tmethod: '${GET}', \n\theaders: {}, \n\tbody: '${body}'\n})", { label: 'Requests', type: 'keyword', }, ), snippetCompletion( "const pid = await Plugins.ExecBackground(\n\t'${path}', \n\t[${args}], \n\tasync (out) => {\n\t\t${}\n\t}, \n\tasync () => {\n\t\t${}\n\t}\n)", { label: 'ExecBackground', type: 'keyword', }, ), ] const completions = [ /** * Global methods include all APIs of `Plugins` and `Plugin Metadata` */ scopeCompletionSource({ ...window, Plugin: pluginScope }), /** * Code Snippets */ completeFromList(snippetsCompletions), /** * Locally Defined */ (context: CompletionContext) => { const word = context.matchBefore(/\w*/) if (!word || context.explicit) return null const codeCompletion = localCompletionSource(context) || { options: [] } return { from: word.from, options: codeCompletion.options, } }, ] return completions } ================================================ FILE: frontend/src/utils/env.ts ================================================ export const APP_TITLE = import.meta.env.VITE_APP_TITLE export const APP_VERSION = import.meta.env.VITE_APP_VERSION export const APP_VERSION_API = import.meta.env.VITE_APP_VERSION_API export const APP_LOCALES_URL = import.meta.env.VITE_APP_LOCALES_URL export const PROJECT_URL = import.meta.env.VITE_APP_PROJECT_URL export const TG_GROUP = import.meta.env.VITE_APP_TG_GROUP export const TG_CHANNEL = import.meta.env.VITE_APP_TG_CHANNEL export const isDev = import.meta.env.DEV ================================================ FILE: frontend/src/utils/eventBus.ts ================================================ type EventMap = { profileChange: { id: string } subscriptionChange: { id: string } subscriptionsChange: void rulesetChange: { id: string } rulesetsChange: void } class TypedEventBus> { private handlers: { [K in keyof Events]?: ((data: Events[K]) => void)[] } = {} on(event: K, handler: (data: Events[K]) => void) { const list = this.handlers[event] || [] list.push(handler) this.handlers[event] = list } off(event: K, handler: (data: Events[K]) => void) { const list = this.handlers[event] if (!list) return this.handlers[event] = list.filter((h) => h !== handler) } emit(event: K, data: Events[K]) { const list = this.handlers[event] if (!list) return list.forEach((h) => h(data)) } } export const eventBus = new TypedEventBus() ================================================ FILE: frontend/src/utils/format.ts ================================================ import i18n from '@/lang' export function formatBytes(bytes: number, decimals: number = 1): string { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const i = Math.max(0, Math.floor(Math.log(bytes) / Math.log(k))) const formattedValue = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) return `${formattedValue} ${sizes[i]}` } export function formatRelativeTime(d: string | number) { const formatter = new Intl.RelativeTimeFormat(i18n.global.locale.value, { numeric: 'auto' }) const date = new Date(d) const now = Date.now() const diffMs = date.getTime() - now const isSameDay = formatDate(d, 'YYYY-MM-DD') === formatDate(now, 'YYYY-MM-DD') // now if (diffMs === 0) return formatter.format(0, 'second') const units: { unit: Intl.RelativeTimeFormatUnit; threshold: number }[] = [ { unit: 'year', threshold: 365 * 24 * 60 * 60 * 1000 }, { unit: 'month', threshold: 30 * 24 * 60 * 60 * 1000 }, { unit: 'day', threshold: 24 * 60 * 60 * 1000 }, { unit: 'hour', threshold: 60 * 60 * 1000 }, { unit: 'minute', threshold: 60 * 1000 }, { unit: 'second', threshold: 1000 }, ] for (const { unit, threshold } of units) { if (unit === 'day' && isSameDay) continue const amount = Math.round(diffMs / threshold) if (Math.abs(amount) > 0) return formatter.format(amount, unit) } return formatter.format(Math.round(diffMs / 1000), 'second') } export function formatDate(timestamp: number | string, format: string) { const date = new Date(timestamp) const map: Record = { YYYY: date.getFullYear(), MM: String(date.getMonth() + 1).padStart(2, '0'), DD: String(date.getDate()).padStart(2, '0'), HH: String(date.getHours()).padStart(2, '0'), mm: String(date.getMinutes()).padStart(2, '0'), ss: String(date.getSeconds()).padStart(2, '0'), } return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (matched) => map[matched]) } ================================================ FILE: frontend/src/utils/generator.ts ================================================ import { parse } from 'yaml' import { ReadFile, WriteFile } from '@/bridge' import { CoreConfigFilePath } from '@/constant/kernel' import { DnsServer, Inbound, LogLevel, Outbound, RuleAction, RulesetType, RuleType, Strategy, } from '@/enums/kernel' import { Branch } from '@/enums/app' import { useAppSettingsStore, usePluginsStore, useRulesetsStore, useSubscribesStore, } from '@/stores' import { deepAssign, deepClone, APP_TITLE, buildSmartRegExp } from '@/utils' const _generateRule = (rule: IRule | IDNSRule, rule_set: IRuleSet[], inbounds: IInbound[]) => { const getInbound = (id: string) => inbounds.find((v) => v.id === id)?.tag const getRuleset = (id: string) => rule_set.find((v) => v.id === id)?.tag const extra: Recordable = { action: rule.action, invert: rule.invert ? true : undefined } if (rule.type === RuleType.Inline) { deepAssign(extra, JSON.parse(rule.payload)) } else if (rule.type === RuleType.RuleSet) { extra[rule.type] = rule.payload.split(',').map((id) => getRuleset(id)) } else if (rule.type === RuleType.Inbound) { extra[rule.type] = getInbound(rule.payload) } else if ([RuleType.IpIsPrivate, RuleType.IpAcceptAny].includes(rule.type as any)) { extra[rule.type] = rule.payload === 'true' } else if (rule.type === RuleType.ClashMode) { extra[rule.type] = rule.payload } else { extra[rule.type] = String(rule.payload) .split(',') .map((val) => { if ([RuleType.Port, RuleType.SourcePort].includes(rule.type as any)) { return Number(val) } return val }) if (extra[rule.type].length === 1) { extra[rule.type] = extra[rule.type][0] } } return extra } const generateExperimental = (experimental: IExperimental, outbounds: IOutbound[]) => { const getOutbound = (id: string) => outbounds.find((v) => v.id === id)?.tag return { clash_api: { ...experimental.clash_api, external_ui_download_detour: getOutbound(experimental.clash_api.external_ui_download_detour), }, cache_file: experimental.cache_file, } } const generateInbounds = (inbounds: IInbound[]) => { return inbounds.flatMap((inbound) => { if (!inbound.enable) return [] if (inbound.type !== Inbound.Tun) { const users = inbound[inbound.type]!.users.map((user) => ({ username: user.split(':')[0], password: user.split(':')[1], })) return { type: inbound.type, tag: inbound.tag, ...inbound[inbound.type]!.listen, users: users.length > 0 ? users : undefined, } } if (inbound.type === Inbound.Tun) { return { type: inbound.type, tag: inbound.tag, ...inbound.tun!, route_address: inbound.tun!.route_address?.length ? inbound.tun!.route_address : undefined, route_exclude_address: inbound.tun!.route_exclude_address?.length ? inbound.tun!.route_exclude_address : undefined, } } }) } const generateOutbounds = async (outbounds: IOutbound[]) => { const result: Recordable[] = [] const SubscriptionCache: Recordable = {} const proxiesSet = new Set() const builtInProxiesSet = new Set() const createTagMatcher = (include: string, exclude: string) => { const includeRegex = include ? buildSmartRegExp(include) : null const excludeRegex = exclude ? buildSmartRegExp(exclude) : null return (tag: string) => { const flag1 = includeRegex ? includeRegex.test(tag) : true const flag2 = excludeRegex ? excludeRegex.test(tag) : false return flag1 && !flag2 } } const subscribesStore = useSubscribesStore() for (const outbound of outbounds) { const _outbound: Recordable = { type: outbound.type, tag: outbound.tag, } if (outbound.type === Outbound.Urltest) { _outbound.url = outbound.url _outbound.interval = outbound.interval _outbound.tolerance = outbound.tolerance } if (outbound.type === Outbound.Selector || outbound.type === Outbound.Urltest) { _outbound.interrupt_exist_connections = outbound.interrupt_exist_connections _outbound.outbounds = [] const isTagMatching = createTagMatcher(outbound.include, outbound.exclude) for (const proxy of outbound.outbounds) { if (proxy.type === 'Built-in') { if ([Outbound.Direct, Outbound.Block].includes(proxy.id as Outbound)) { builtInProxiesSet.add(proxy.id) } _outbound.outbounds.push(proxy.tag) } else { const subId = proxy.type === 'Subscription' ? proxy.id : proxy.type if (!SubscriptionCache[subId]) { const sub = subscribesStore.getSubscribeById(subId) if (sub) { const subStr = await ReadFile(sub.path) const proxies = JSON.parse(subStr) SubscriptionCache[subId] = proxies } } if (proxy.type === 'Subscription') { _outbound.outbounds.push( ...SubscriptionCache[subId]!.map((v) => v.tag).filter((tag) => isTagMatching(tag)), ) SubscriptionCache[subId]!.forEach((v) => proxiesSet.add(v)) } else { const _proxy = SubscriptionCache[subId]!.find((v) => v.tag === proxy.tag) if (_proxy && isTagMatching(_proxy.tag)) { _outbound.outbounds.push(_proxy.tag) proxiesSet.add(_proxy) } } } } } result.push(_outbound) } result.push(...proxiesSet) result.push(...Array.from(builtInProxiesSet).map((v) => ({ type: v, tag: v }))) return result } const generateRoute = (route: IRoute, inbounds: IInbound[], outbounds: IOutbound[], dns: IDNS) => { const getOutbound = (id: string) => outbounds.find((v) => v.id === id)?.tag const getDnsServer = (id: string) => dns.servers.find((v) => v.id === id)?.tag const isInboundEnabled = (id: string) => inbounds.find((v) => v.id === id)?.enable const rulesetsStore = useRulesetsStore() const extra: Recordable = {} if (!route.auto_detect_interface) { extra.default_interface = route.default_interface } return { rules: route.rules.flatMap((rule) => { if (rule.type === RuleType.InsertionPoint || !rule.enable) { return [] } if (rule.type === RuleType.Inbound && !isInboundEnabled(rule.payload)) { return [] } const extra: Recordable = _generateRule(rule, route.rule_set, inbounds) if (rule.action === RuleAction.Route) { extra.outbound = getOutbound(rule.outbound) } else if (rule.action === RuleAction.RouteOptions) { deepAssign(extra, JSON.parse(rule.outbound)) } else if (rule.action === RuleAction.Reject) { extra.method = rule.outbound } else if (rule.action === RuleAction.Sniff) { if (rule.sniffer.length) { extra.sniffer = rule.sniffer } } else if (rule.action === RuleAction.Resolve) { if (rule.strategy !== Strategy.Default) { extra.strategy = rule.strategy } extra.server = getDnsServer(rule.server) } if (rule.invert) { extra.invert = true } return extra }), rule_set: route.rule_set.map((ruleset) => { const extra: Recordable = {} if (ruleset.type === RuleType.Inline) { extra.rules = JSON.parse(ruleset.rules) } else if (ruleset.type === RulesetType.Local) { const _ruleset = rulesetsStore.getRulesetById(ruleset.path) extra.path = _ruleset?.path.replace('data/', '../') extra.format = ruleset.format } else if (ruleset.type === RulesetType.Remote) { extra.url = ruleset.url extra.format = ruleset.format extra.download_detour = getOutbound(ruleset.download_detour) if (ruleset.update_interval) { extra.update_interval = ruleset.update_interval } } return { tag: ruleset.tag, type: ruleset.type, ...extra, } }), auto_detect_interface: route.auto_detect_interface, find_process: route.find_process ? true : undefined, final: getOutbound(route.final), default_domain_resolver: { server: getDnsServer(route.default_domain_resolver.server), }, ...extra, } } const generateDns = ( dns: IDNS, rule_set: IRuleSet[], inbounds: IInbound[], outbounds: IOutbound[], ) => { const getOutbound = (id: string) => outbounds.find((v) => v.id === id) const getDnsServer = (id: string) => dns.servers.find((v) => v.id === id)?.tag const extra: Recordable = {} if (dns.strategy !== Strategy.Default) { extra.strategy = dns.strategy } if (dns.client_subnet) { extra.client_subnet = dns.client_subnet } return { servers: dns.servers.flatMap((server) => { const extra: Recordable = {} if ( [ DnsServer.Local, DnsServer.Tcp, DnsServer.Udp, DnsServer.Tls, DnsServer.Quic, DnsServer.Https, DnsServer.H3, DnsServer.Dhcp, ].includes(server.type as any) ) { if (server.detour) { const outbound = getOutbound(server.detour) if (outbound?.type !== Outbound.Direct) { extra.detour = outbound?.tag } } server.domain_resolver && (extra.domain_resolver = getDnsServer(server.domain_resolver)) if ( [ DnsServer.Tcp, DnsServer.Udp, DnsServer.Tls, DnsServer.Quic, DnsServer.Https, DnsServer.H3, ].includes(server.type as any) ) { server.server_port && (extra.server_port = Number(server.server_port)) extra.server = server.server if ([DnsServer.Https, DnsServer.H3].includes(server.type as any)) { server.path && (extra.path = server.path) } } } if (server.type === DnsServer.Hosts) { extra.path = server.hosts_path.reduce((p, c) => p.concat(c.split(',')), [] as string[]) extra.predefined = Object.entries(server.predefined).reduce( (p, [k, v]) => ({ ...p, [k]: v.split(',') }), {}, ) } else if (server.type === DnsServer.Dhcp) { server.interface && (extra.interface = server.interface) } else if (server.type === DnsServer.FakeIP) { server.inet4_range && (extra.inet4_range = server.inet4_range) server.inet6_range && (extra.inet6_range = server.inet6_range) } return { tag: server.tag, type: server.type, ...extra, } }), rules: dns.rules.flatMap((rule) => { if (rule.type === RuleType.InsertionPoint || !rule.enable) { return [] } const extra: Recordable = _generateRule(rule, rule_set, inbounds) if (rule.type === RuleType.Inline && rule.payload.includes('__is_fake_ip')) { if (!dns.servers.find((v) => v.type === DnsServer.FakeIP)) { return [] } delete extra.__is_fake_ip } if ([RuleAction.Route, RuleAction.RouteOptions].includes(rule.action as any)) { rule.disable_cache && (extra.disable_cache = rule.disable_cache) rule.client_subnet && (extra.client_subnet = rule.client_subnet) if (rule.action === RuleAction.Route) { extra.server = getDnsServer(rule.server) if (rule.strategy !== Strategy.Default) { extra.strategy = rule.strategy } } } if ([RuleAction.RouteOptions, RuleAction.Predefined].includes(rule.action as any)) { deepAssign(extra, JSON.parse(rule.server)) } if (rule.action === RuleAction.Reject) { extra.method = rule.server } return extra }), disable_cache: dns.disable_cache, disable_expire: dns.disable_expire, independent_cache: dns.independent_cache, final: getDnsServer(dns.final), ...extra, } } export const generateDnsServerURL = (dnsServer: IDNSServer) => { const { type, server_port, path, server, interface: _interface } = dnsServer let address = '' if (type == DnsServer.Https) { address = `https://${server}${server_port ? ':' + server_port : ''}${path ? path : ''}` } else if (type == DnsServer.H3) { address = `h3://${server}${server_port ? ':' + server_port : ''}${path ? path : ''}` } else if (type == DnsServer.Dhcp) { address = `dhcp://${_interface}` } else if (type == DnsServer.FakeIP) { address = 'fake-ip://' + (dnsServer.inet4_range ? dnsServer.inet4_range : '') + (dnsServer.inet6_range ? (dnsServer.inet4_range ? ',' : '') + dnsServer.inet6_range : '') } else if (type === DnsServer.Hosts) { address = 'hosts' } else if (type === DnsServer.Local) { address = 'local' } else { address = `${type}://${server}${server_port ? ':' + server_port : ''}` } return address } const _adaptToStableBranch = (_: Recordable) => {} type GenerateConfigOptions = { enableStableConfigCompat?: boolean enablePluginProcessing?: boolean enableMixinProcessing?: boolean enableScriptProcessing?: boolean } export const generateConfig = async ( originalProfile: IProfile, options: GenerateConfigOptions = {}, ) => { if (typeof options === 'boolean') { options = { enableStableConfigCompat: options } } const appSettings = useAppSettingsStore() const isMainBranch = appSettings.app.kernel.branch === Branch.Main const { enableStableConfigCompat = isMainBranch, enablePluginProcessing = true, enableMixinProcessing = true, enableScriptProcessing = true, } = options const profile = deepClone(originalProfile) // step 1 let config: Recordable = { log: profile.log, experimental: generateExperimental(profile.experimental, profile.outbounds), inbounds: generateInbounds(profile.inbounds), outbounds: await generateOutbounds(profile.outbounds), route: generateRoute(profile.route, profile.inbounds, profile.outbounds, profile.dns), dns: generateDns(profile.dns, profile.route.rule_set, profile.inbounds, profile.outbounds), } // adapt to stable branch if (enableStableConfigCompat) { _adaptToStableBranch(config) } // step 2 if (enablePluginProcessing) { const pluginsStore = usePluginsStore() config = await pluginsStore.onGenerateTrigger(config, originalProfile) } // step 3 if (enableMixinProcessing) { const { priority, config: mixin } = originalProfile.mixin if (priority === 'mixin') { deepAssign(config, parse(mixin)) } else if (priority === 'gui') { deepAssign(config, deepAssign(parse(mixin), config)) } } // step 4 if (enableScriptProcessing) { const fn = new window.AsyncFunction( 'config', `${originalProfile.script.code}; return await onGenerate(config)`, ) try { config = await fn(config) } catch (error: any) { throw error.message || error } if (typeof config !== 'object') { throw 'Wrong result' } } return config } export const generateConfigFile = async ( profile: IProfile, beforeWrite: (config: any) => Promise, ) => { const header = `DO NOT EDIT - Generated by ${APP_TITLE}` const _config = await generateConfig(profile) const config = await beforeWrite(_config) config.log.disabled = false config.log.output = '' if (![LogLevel.Trace, LogLevel.Debug, LogLevel.Info].includes(config.log.level)) { config.log.level = LogLevel.Info } config.experimental.cache_file.path = 'cache.db' await WriteFile(CoreConfigFilePath, JSON.stringify({ $schema: header, ...config }, null, 2)) } ================================================ FILE: frontend/src/utils/helper.ts ================================================ import { deleteConnection, getConnections, useProxy } from '@/api/kernel' import { AbsolutePath, Exec, ExitApp, ReadFile, WindowReloadApp, WriteFile } from '@/bridge' import { CoreWorkingDirectory } from '@/constant/kernel' import { RulesetFormat } from '@/enums/kernel' import i18n from '@/lang' import { type ProxyType, useAppSettingsStore, useAppStore, useEnvStore, useKernelApiStore, usePluginsStore, useRulesetsStore, } from '@/stores' import { ignoredError, message, confirm } from '@/utils' // Permissions Helper export const SwitchPermissions = async (enable: boolean) => { const { appPath } = useEnvStore().env const args = enable ? [ 'add', 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers', '/v', appPath, '/t', 'REG_SZ', '/d', 'RunAsAdmin', '/f', ] : [ 'delete', 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers', '/v', appPath, '/f', ] await Exec('reg', args, { Convert: true }) } export const CheckPermissions = async () => { const { appPath } = useEnvStore().env try { const out = await Exec( 'reg', [ 'query', 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers', '/v', appPath, '/t', 'REG_SZ', ], { Convert: true }, ) return out.includes('RunAsAdmin') } catch { return false } } export const GrantTUNPermission = async (path: string) => { const { os } = useEnvStore().env const absPath = await AbsolutePath(path) if (os === 'darwin') { const osaScript = `chown root:admin ${absPath}\nchmod +sx ${absPath}` const bashScript = `osascript -e 'do shell script "${osaScript}" with administrator privileges'` await Exec('bash', ['-c', bashScript]) } else if (os === 'linux') { await Exec('pkexec', [ 'setcap', 'cap_net_bind_service,cap_net_admin,cap_dac_override=+ep', absPath, ]) } } export const RunWithPowerShell = async ( path: string, args: string[] = [], options: { admin?: boolean; hidden?: boolean; wait?: boolean }, ) => { const { admin = false, hidden = false, wait = true, ...others } = options const psArgs: string[] = [] let command = `Start-Process -FilePath "${path}"` if (args.length > 0) { const argList = args.map((a) => `"${a.replace(/"/g, '""')}"`).join(',') command += ` -ArgumentList ${argList}` } if (admin) { command += ' -Verb RunAs' } if (hidden) { command += ' -WindowStyle Hidden' } if (wait) { command += ' -Wait' } psArgs.push('-NoProfile', '-Command', command) await Exec('powershell', psArgs, { Convert: true, ...others }) } // SystemProxy Helper export const SetSystemProxy = async ( enable: boolean, server: string, proxyType: ProxyType = 'mixed', bypass = '', ) => { const { os } = useEnvStore().env const handler = { windows: setWindowsSystemProxy, darwin: setDarwinSystemProxy, linux: setLinuxSystemProxy, }[os] await handler?.(server, enable, proxyType, bypass) } async function setWindowsSystemProxy( server: string, enabled: boolean, proxyType: ProxyType, bypass: string, ) { const p1 = ignoredError(Exec, 'reg', [ 'add', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', enabled ? '1' : '0', '/f', ]) const p2 = ignoredError(Exec, 'reg', [ 'add', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', 'ProxyServer', '/d', enabled ? (proxyType === 'socks' ? 'socks=' + server : server) : '', '/f', ]) const p3 = ignoredError(Exec, 'reg', [ 'add', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', 'ProxyOverride', '/d', bypass .split(';') .map((v) => v.trim()) .filter(Boolean) .join(';'), '/f', ]) await Promise.all([p1, p2, p3]) } async function setDarwinSystemProxy( server: string, enabled: boolean, proxyType: ProxyType, bypass: string, ) { async function _set(device: string) { const state = enabled ? 'on' : 'off' const httpState = ['mixed', 'http'].includes(proxyType) ? state : 'off' const socksState = ['mixed', 'socks'].includes(proxyType) ? state : 'off' const p1 = ignoredError(Exec, 'networksetup', ['-setwebproxystate', device, httpState]) const p2 = ignoredError(Exec, 'networksetup', ['-setsecurewebproxystate', device, httpState]) const p3 = ignoredError(Exec, 'networksetup', [ '-setsocksfirewallproxystate', device, socksState, ]) const p4 = ignoredError(Exec, 'networksetup', [ '-setproxybypassdomains', device, ...bypass .split(';') .map((v) => v.trim()) .filter(Boolean), ]) const [serverName, serverPort] = server.split(':') as [string, string] const promises = [p1, p2, p3, p4] if (httpState === 'on') { const p1 = ignoredError(Exec, 'networksetup', [ '-setwebproxy', device, serverName, serverPort, ]) const p2 = ignoredError(Exec, 'networksetup', [ '-setsecurewebproxy', device, serverName, serverPort, ]) promises.push(p1, p2) } if (socksState === 'on') { const p1 = ignoredError(Exec, 'networksetup', [ '-setsocksfirewallproxy', device, serverName, serverPort, ]) promises.push(p1) } await Promise.all(promises) } const p1 = _set('Ethernet') const p2 = _set('Wi-Fi') await Promise.all([p1, p2]) } async function setLinuxSystemProxy( server: string, enabled: boolean, proxyType: ProxyType, bypass: string, ) { const [serverName, serverPort] = server.split(':') as [string, string] const httpEnabled = enabled && ['mixed', 'http'].includes(proxyType) const socksEnabled = enabled && ['mixed', 'socks'].includes(proxyType) const desktop = (await Exec('sh', ['-c', 'echo $XDG_CURRENT_DESKTOP'])).trim() if (desktop.includes('KDE')) { const p1 = ignoredError(Exec, 'kwriteconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'ProxyType', enabled ? '1' : '0', ]) const p2 = ignoredError(Exec, 'kwriteconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'httpProxy', httpEnabled ? `http://${server}` : '', ]) const p3 = ignoredError(Exec, 'kwriteconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'httpsProxy', httpEnabled ? `http://${server}` : '', ]) const p4 = ignoredError(Exec, 'kwriteconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'socksProxy', socksEnabled ? `socks://${server}` : '', ]) const p5 = ignoredError(Exec, 'kwriteconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'NoProxyFor', bypass .split(';') .map((v) => v.trim()) .filter(Boolean) .join(','), ]) await Promise.all([p1, p2, p3, p4, p5]) } else if (['GNOME', 'XFCE'].includes(desktop)) { const p1 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy', 'mode', enabled ? 'manual' : 'none', ]) const p2 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy.http', 'host', httpEnabled ? serverName : '', ]) const p3 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy.http', 'port', httpEnabled ? serverPort : '0', ]) const p4 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy.https', 'host', httpEnabled ? serverName : '', ]) const p5 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy.https', 'port', httpEnabled ? serverPort : '0', ]) const p6 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy.socks', 'host', socksEnabled ? serverName : '', ]) const p7 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy.socks', 'port', socksEnabled ? serverPort : '0', ]) const p8 = ignoredError(Exec, 'gsettings', [ 'set', 'org.gnome.system.proxy', 'ignore-hosts', `[${bypass .split(';') .map((v) => v.trim()) .filter(Boolean) .map((v) => `'${v}'`) .join(',')}]`, ]) await Promise.all([p1, p2, p3, p4, p5, p6, p7, p8]) } } export const GetSystemProxy = async () => { const { os } = useEnvStore().env try { if (os === 'windows') { const out1 = await Exec('reg', [ 'query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', 'ProxyEnable', '/t', 'REG_DWORD', ]) if (/REG_DWORD\s+0x0/.test(out1)) return '' const out2 = await Exec('reg', [ 'query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', 'ProxyServer', '/t', 'REG_SZ', ]) const regex = /ProxyServer\s+REG_SZ\s+(\S+)/ const match = out2.match(regex) return match ? (match?.[1]?.startsWith('socks') ? match[1] : 'http://' + match[1]) : '' } if (os === 'darwin') { const out = await Exec('scutil', ['--proxy']) const regex = /(?:HTTPEnable|HTTPPort|HTTPProxy|SOCKSEnable|SOCKSPort|SOCKSProxy)\s*:\s*([^}\n]+)/g const map: Record = {} let match while ((match = regex.exec(out)) !== null) { const value = match[1]?.trim() const key = (match[0].split(':') as [string, string])[0].trim() map[key] = value } if (map['HTTPEnable'] === '1') { return 'http://' + map['HTTPProxy'] + ':' + map['HTTPPort'] } if (map['SOCKSEnable'] === '1') { return 'socks5://' + map['SOCKSProxy'] + ':' + map['SOCKSPort'] } return '' } if (os === 'linux') { const desktop = (await Exec('sh', ['-c', 'echo $XDG_CURRENT_DESKTOP'])).trim() if (desktop.includes('KDE')) { const out = await Exec('kreadconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'ProxyType', ]) if (out.includes('1')) { const out1 = await Exec('kreadconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'httpProxy', ]) const http = out1.replace(/['"\n]/g, '') if (http) { return http.replace(' ', ':') } const out2 = await Exec('kreadconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'socksProxy', ]) const socks = out2.replace(/['"\n]/g, '') if (socks) { return socks.replace(' ', ':') } } } else if (['GNOME', 'XFCE'].includes(desktop)) { const out = await Exec('gsettings', ['get', 'org.gnome.system.proxy', 'mode']) if (out.includes('none')) { return '' } if (out.includes('manual')) { const out1 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.http', 'host']) const out2 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.http', 'port']) const httpHost = out1.replace(/['"\n]/g, '') const httpPort = out2.replace(/['"\n]/g, '') if (httpHost && httpPort !== '0') { return 'http://' + httpHost + ':' + httpPort } const out3 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.socks', 'host']) const out4 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.socks', 'port']) const socksHost = out3.replace(/['"\n]/g, '') const socksPort = out4.replace(/['"\n]/g, '') if (socksHost && socksPort !== '0') { return 'socks5://' + socksHost + ':' + socksPort } } } } } catch (error) { console.log('error', error) } return '' } export const GetSystemProxyBypass = async () => { const { os } = useEnvStore().env if (os === 'windows') { const out = await ignoredError(Exec, 'reg', [ 'query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', 'ProxyOverride', ]) if (!out) return '' return out.match(/ProxyOverride\s+REG_SZ\s+(\S+)/)?.[1] || '' } if (os === 'darwin') { async function _get(device: string) { const out = await ignoredError(Exec, 'networksetup', ['-getproxybypassdomains', device]) if (!out) return [] return out.trim().split('\n').filter(Boolean) } const res = await Promise.all([_get('Ethernet'), _get('Wi-Fi')]) return res.flat().join(';') } if (os === 'linux') { const desktop = (await Exec('sh', ['-c', 'echo $XDG_CURRENT_DESKTOP'])).trim() if (desktop.includes('KDE')) { const out = await ignoredError(Exec, 'kreadconfig5', [ '--file', 'kioslaverc', '--group', 'Proxy Settings', '--key', 'NoProxyFor', ]) if (!out) return '' return out .trim() .split(',') .map((v) => v.trim()) .join(';') } else if (['GNOME', 'XFCE'].includes(desktop)) { const out = await ignoredError(Exec, 'gsettings', [ 'get', 'org.gnome.system.proxy', 'ignore-hosts', ]) if (!out) return '' const arrStart = out.indexOf('[') const arrStr = arrStart >= 0 ? out.slice(arrStart) : out const jsonLike = arrStr.replace(/'/g, '"') const arr = (await ignoredError(JSON.parse, jsonLike)) ?? [] if (!Array.isArray(arr)) return '' return arr.join(';') } } return '' } const proxy_cache: { proxyPromise: Promise | null; lastAccessTime: number } = { proxyPromise: null, lastAccessTime: 0, } export const GetSystemOrKernelProxy = async () => { if (useKernelApiStore().running) { const kernelProxy = useKernelApiStore().getProxyPort() if (kernelProxy !== undefined) { if (kernelProxy.proxyType === 'socks') { return `socks5://127.0.0.1:${kernelProxy.port}` } return `http://127.0.0.1:${kernelProxy.port}` } } if (proxy_cache.proxyPromise && Date.now() - proxy_cache.lastAccessTime < 1000) { return proxy_cache.proxyPromise } proxy_cache.lastAccessTime = Date.now() proxy_cache.proxyPromise = GetSystemProxy() return proxy_cache.proxyPromise } export const QuerySchTask = async (taskName: string) => { await Exec('Schtasks', ['/Query', '/TN', taskName, '/XML'], { Convert: true }) } export const CreateSchTask = async (taskName: string, xmlPath: string) => { const fn = useEnvStore().env.isPrivileged ? Exec : RunWithPowerShell await fn('SchTasks', ['/Create', '/F', '/TN', taskName, '/XML', xmlPath], { admin: true, hidden: true, }) } export const DeleteSchTask = async (taskName: string) => { const fn = useEnvStore().env.isPrivileged ? Exec : RunWithPowerShell await fn('SchTasks', ['/Delete', '/F', '/TN', taskName], { admin: true, hidden: true }) } // Others export const handleUseProxy = async (group: any, proxy: any) => { if (group.type !== 'Selector' || group.now === proxy.name) return const promises: Promise[] = [] const appSettings = useAppSettingsStore() const kernelApiStore = useKernelApiStore() if (appSettings.app.kernel.autoClose) { const { connections } = await getConnections() promises.push( ...(connections || []) .filter((v) => v.chains.includes(group.name)) .map((v) => deleteConnection(v.id)), ) } await useProxy(encodeURIComponent(group.name), proxy.name) await Promise.all(promises) await kernelApiStore.refreshProviderProxies() } export const handleChangeMode = async (mode: 'direct' | 'global' | 'rule') => { const kernelApiStore = useKernelApiStore() if (mode === kernelApiStore.config.mode) return kernelApiStore.updateConfig('mode', mode) const { connections } = await getConnections() const promises = (connections || []).map((v) => deleteConnection(v.id)) await Promise.all(promises) } export const addToRuleSet = async ( id: 'direct' | 'reject' | 'proxy', payloads: Record[], ) => { const path = `data/rulesets/${id}.json` const rulesetsStoe = useRulesetsStore() let ruleset = rulesetsStoe.getRulesetById(id) if (!ruleset) { ruleset = { id, tag: id, updateTime: 0, type: 'Manual', format: RulesetFormat.Source, url: '', path, count: 0, disabled: false, } await rulesetsStoe.addRuleset(ruleset) } const content = (await ignoredError(ReadFile, path)) || '{ "version": 1, "rules": [] }' const { rules = [] } = JSON.parse(content) rules[0] = rules[0] || {} payloads.forEach((payload) => { if (payload.domain) { rules[0].domain = [...new Set((rules[0].domain || []).concat(payload.domain))] } else if (payload.ip_cidr) { rules[0].ip_cidr = [...new Set((rules[0].ip_cidr || []).concat(payload.ip_cidr))] } else if (payload.process_path) { rules[0].process_path = [ ...new Set((rules[0].process_path || []).concat(payload.process_path)), ] } else if (payload.domain_suffix) { rules[0].domain_suffix = [ ...new Set((rules[0].domain_suffix || []).concat(payload.domain_suffix)), ] } }) await WriteFile(path, JSON.stringify({ version: 1, rules }, null, 2)) await rulesetsStoe.updateRuleset(id) } export const reloadApp = async () => { const { t } = i18n.global const appStore = useAppStore() const pluginsStore = usePluginsStore() appStore.isAppReloading = true let timedout = false const { destroy } = message.info('titlebar.reloadPending', 10 * 60 * 1000) const timeoutId = setTimeout(async () => { timedout = true appStore.isAppReloading = false destroy() confirm('Warning', t('titlebar.reloadTimeout')).then(WindowReloadApp) }, 10_000) try { await pluginsStore.onReloadTrigger() if (!timedout) { clearTimeout(timeoutId) WindowReloadApp() } } catch (err: any) { clearTimeout(timeoutId) confirm('Error', t('titlebar.reloadError', { reason: err })).then(WindowReloadApp) } appStore.isAppReloading = false destroy() } export const exitApp = async () => { const { t } = i18n.global const appStore = useAppStore() const envStore = useEnvStore() const pluginsStore = usePluginsStore() const appSettings = useAppSettingsStore() const kernelApiStore = useKernelApiStore() appStore.isAppExiting = true let timedout = false const { destroy } = message.info('titlebar.exitPending', 10 * 60 * 1000) const timeoutId = setTimeout(async () => { timedout = true appStore.isAppExiting = false destroy() confirm('Warning', t('titlebar.exitTimeout')).then(ExitApp) }, 10_000) try { if (kernelApiStore.running && appSettings.app.closeKernelOnExit) { await kernelApiStore.stopCore() if (appSettings.app.autoSetSystemProxy) { await envStore.clearSystemProxy() } } await pluginsStore.onShutdownTrigger() if (!timedout) { clearTimeout(timeoutId) ExitApp() } } catch (err: any) { clearTimeout(timeoutId) confirm('Error', t('titlebar.exitError', { reason: err })).then(ExitApp) } appStore.isAppExiting = false destroy() } export const getKernelFileName = (isAlpha = false) => { const envStore = useEnvStore() const { os } = envStore.env const fileSuffix = { windows: '.exe', linux: '', darwin: '' }[os] const latest = isAlpha ? '-latest' : '' return `sing-box${latest}${fileSuffix}` } export const getKernelAssetFileName = (version: string) => { const envStore = useEnvStore() const { os, arch } = envStore.env const suffix = { windows: '.zip', linux: '.tar.gz', darwin: '.tar.gz' }[os] return `sing-box-${version}-${os}-${arch}${suffix}` } export const processMagicVariables = (str: string) => { const { env } = useEnvStore() let result = str Object.entries({ $APP_BASE_PATH: env.basePath, $CORE_BASE_PATH: CoreWorkingDirectory, }).forEach(([source, target]) => { result = result.replaceAll(source, target) }) return result } export const getKernelRuntimeEnv = (isAlpha = false) => { const appSettings = useAppSettingsStore() const { env } = isAlpha ? appSettings.app.kernel.alpha : appSettings.app.kernel.main return Object.entries(env).reduce((p, [key, value]) => { p[key] = processMagicVariables(value) return p }, {} as Recordable) } export const getKernelRuntimeArgs = (isAlpha = false) => { const appSettings = useAppSettingsStore() const { args } = isAlpha ? appSettings.app.kernel.alpha : appSettings.app.kernel.main return args.map((arg) => processMagicVariables(arg)) } ================================================ FILE: frontend/src/utils/index.ts ================================================ export * from './env' export * from './format' export * from './generator' export * from './restorer' export * from './is' export * from './others' export * from './helper' export * from './tray' export * from './completion' export * from './interaction' export * from './eventBus' export * from './migration' ================================================ FILE: frontend/src/utils/interaction.ts ================================================ import { render, h, type VNode, nextTick } from 'vue' import i18n from '@/lang' import { APP_TITLE, sampleID } from '@/utils' import ConfirmComp from '@/components/Confirm/index.vue' import MessageComp from '@/components/Message/index.vue' import { useModal } from '@/components/Modal' import PickerComp from '@/components/Picker/index.vue' import PromptComp from '@/components/Prompt/index.vue' import type { ConfirmOptions } from '@/components/Confirm/index.vue' import type { Props as InputProps } from '@/components/Input/index.vue' import type { MessageIcon } from '@/components/Message/index.vue' import type { Props as ModalProps, Slots as ModalSlots } from '@/components/Modal/index.vue' import type { PickerItem } from '@/components/Picker/index.vue' const ContainerCssText = ` position: fixed; z-index: 99999; top: 84px; left: 0; right: 0; display: flex; justify-content: center; max-height: 70%; ` interface MessageInstance { dom: HTMLDivElement vnode: VNode timer: number } const bindAppContext = (vnode: VNode) => { vnode.appContext = window.appInstance._context } class Message { public container: HTMLElement public instances: Record constructor() { const ID = APP_TITLE + '-toast' this.container = document.getElementById(ID) || document.createElement('div') this.container.id = ID this.container.style.cssText = ` position: fixed; z-index: 999999; top: 80px; left: 50%; transform: translateX(-50%); ` document.body.appendChild(this.container) this.instances = {} } private buildMessage = (icon: MessageIcon) => { return (content: string, duration = 3_000, onClose?: () => void) => { const id = sampleID() const dom = document.createElement('div') const onMouseEnter = () => clearTimeout(this.instances[id]!.timer) const onMouseLeave = () => (this.instances[id]!.timer = setTimeout(onDestroy, duration)) const onDestroy = () => { dom.removeEventListener('mouseenter', onMouseEnter) dom.removeEventListener('mouseleave', onMouseLeave) this.destroy(id) } const initInstance = () => { dom.style.cssText = 'display: flex; align-items: center; justify-content: center;' const vnode = h(MessageComp, { icon, content, onClose: () => { onClose?.() onDestroy() }, }) bindAppContext(vnode) this.instances[id] = { dom, vnode, timer: setTimeout(onDestroy, duration), } dom.addEventListener('mouseenter', onMouseEnter) dom.addEventListener('mouseleave', onMouseLeave) this.container.appendChild(dom) render(vnode, dom) } initInstance() return { id, info: (content: string) => this.update(id, content, 'info'), warn: (content: string) => this.update(id, content, 'warn'), error: (content: string) => this.update(id, content, 'error'), success: (content: string) => this.update(id, content, 'success'), update: (content: string, icon?: MessageIcon) => this.update(id, content, icon), destroy: onDestroy, } } } public info = this.buildMessage('info') public warn = this.buildMessage('warn') public error = this.buildMessage('error') public success = this.buildMessage('success') public update = (id: string, content: string, icon?: MessageIcon) => { const instance = this.instances[id] if (instance) { icon && (instance.vnode.component!.props.icon = icon) content && (instance.vnode.component!.props.content = content) } } public destroy = (id: string) => { const instance = this.instances[id] if (instance) { render(null, instance.dom) instance.dom.remove() clearTimeout(instance.timer) delete this.instances[id] } } } class Picker { constructor() {} public single = (title: string, options: PickerItem[], initialValue: T[] = []) => { return this.buildPicker('single', title, options, initialValue) } public multi = (title: string, options: PickerItem[], initialValue: T[] = []) => { return this.buildPicker('multi', title, options, initialValue) } private buildPicker = ( type: PickerType, title: string, options: PickerItem[], initialValue: ValueType[], ): Promise => { return new Promise((resolve, reject) => { const { t } = i18n.global const dom = document.createElement('div') dom.style.cssText = ContainerCssText const vnode = h(PickerComp, { type, title, options, initialValue, onConfirm: resolve, onCancel: () => reject(t('common.canceled')), onFinish: () => { render(null, dom) dom.remove() }, }) bindAppContext(vnode) document.body.appendChild(dom) render(vnode, dom) }) } } const buildConfirm = ( title: string, message: string, options: ConfirmOptions = { type: 'text' }, cancel = true, ) => { return new Promise((resolve, reject) => { const { t } = i18n.global const dom = document.createElement('div') dom.style.cssText = ContainerCssText const vnode = h(ConfirmComp, { title, message, options, cancel, onConfirm: resolve, onCancel: () => reject(t('common.canceled')), onFinish: () => { render(null, dom) dom.remove() }, }) bindAppContext(vnode) document.body.appendChild(dom) render(vnode, dom) }) } export const prompt = ( title: string, initialValue: string | number = '', props: Partial = {}, ) => { const { t } = i18n.global return new Promise((resolve, reject) => { const dom = document.createElement('div') dom.style.cssText = ContainerCssText const vnode = h(PromptComp, { title, initialValue, props, onSubmit: resolve, onCancel: () => reject(t('common.canceled')), onFinish: () => { render(null, dom) dom.remove() }, }) bindAppContext(vnode) document.body.appendChild(dom) render(vnode, dom) }) } export const alert = ( title: string, message: string, options: ConfirmOptions = { type: 'text' }, ) => { return buildConfirm(title, message, options, false) } export const confirm = ( title: string, message: string, options: ConfirmOptions = { type: 'text' }, ) => { return buildConfirm(title, message, options) } export const modal = (options: ModalProps = {}, slots: ModalSlots = {}) => { const [Modal, api] = useModal(options, slots) const vnode = h(Modal) bindAppContext(vnode) const container = document.createElement('div') document.body.appendChild(container) render(vnode, container) const destroy = () => { api.close() nextTick(() => { render(null, container) container.remove() }) } const powerApi = { ...api, destroy } return powerApi } export const picker = new Picker() export const message = new Message() ================================================ FILE: frontend/src/utils/is.ts ================================================ import { Cron } from 'croner' import { parse } from 'yaml' import { normalizeBase64 } from './others' export const isValidBase64 = (str: string) => { if (typeof str !== 'string') return false if (str === '' || str.trim() === '') { return false } // Accept URL-safe base64 and ignore line breaks/spaces in subscription responses. const normalized = normalizeBase64(str) try { atob(normalized) return true } catch { return false } } export const isValidSubYAML = (str: string) => { if (typeof str !== 'string') return false try { const { proxies } = parse(str) return !!proxies } catch { return false } } export const isValidSubJson = (str: string) => { if (typeof str !== 'string') return false try { const { outbounds } = JSON.parse(str) return !!outbounds } catch { return false } } export const isValidPaylodYAML = (str: string) => { try { const { payload } = parse(str) return !!payload } catch { return false } } export const isValidRulesJson = (str: string) => { try { const { rules } = JSON.parse(str) return !!rules } catch { return false } } export const isValidIPv4 = (ip: string) => /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip) export const isValidIPv6 = (ip: string) => /^([\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:):([\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){3}:([\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/.test( ip, ) export const isValidJson = (str: string) => { try { return !!JSON.parse(str) } catch { return false } } export const isNumber = (v: any) => typeof v === 'number' export const isValidCron = (pattern: string) => { try { const instance = new Cron(pattern, { paused: true }) return { ok: true, reason: null, instance: instance } } catch (error: any) { return { ok: false, reason: error.message || error, instance: null } } } ================================================ FILE: frontend/src/utils/migration.ts ================================================ export const migrateProfiles = async (profiles: IProfile[], save: () => Promise) => { let needSync = false profiles.forEach((profile) => { profile.dns.rules.forEach((rule) => { if (typeof rule.enable === 'undefined') { rule.enable = true needSync = true } }) profile.route.rules.forEach((rule) => { if (typeof rule.enable === 'undefined') { rule.enable = true needSync = true } }) }) if (needSync) await save() } ================================================ FILE: frontend/src/utils/others.ts ================================================ import { stringify } from 'yaml' import { useAppSettingsStore, useEnvStore } from '@/stores' import { APP_TITLE, APP_VERSION } from '@/utils' export const deepClone = (json: T): T => JSON.parse(JSON.stringify(json)) export const omit = (obj: T, props: K[]): Omit => { const result = {} as T const omitSet = new Set(props) for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { if (!omitSet.has(key as unknown as K)) { result[key] = obj[key] } } } return result as Omit } export const omitArray = (arr: T[], fields: K[]): Omit[] => { return arr.map((obj) => { const item: Partial = deepClone(obj) fields.forEach((key) => { delete item[key] }) return item as Omit }) } export const debounce = (fn: (...args: any) => any, wait: number) => { let timer: null | number = null const _debuonce = function (...args: any) { return new Promise((resolve, reject) => { timer && clearTimeout(timer) timer = setTimeout(async () => { try { await fn(...args) resolve(null) } catch (error) { reject(error) } }, wait) }) } _debuonce.cancel = function () { timer && clearTimeout(timer) timer = null } return _debuonce } export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) export const ignoredError = async Promise>( fn: F, ...args: Parameters ): Promise | undefined> => { try { return await fn(...args) } catch { return undefined } } export const sampleID = () => 'ID_' + Math.random().toString(36).substring(2, 10) export const generateSecureKey = (bits = 256) => { const bytes = bits / 8 const array = new Uint8Array(bytes) crypto.getRandomValues(array) return Array.from(array) .map((b) => b.toString(16).padStart(2, '0')) .join('') } export const getValue = (obj: unknown, expr: string): T | undefined => { return expr.split('.').reduce((value, key) => { if (value && typeof value === 'object') { return (value as Record)[key] } return undefined }, obj) as T } type IteratorFn = (item: T, array: T[]) => Promise type PoolController = { pause: () => void; resume: () => void; cancel: () => void } interface RunPoolOptions { shouldPause?: () => Promise shouldCancel?: () => boolean } async function runPool( poolLimit: number, array: T[], iteratorFn: IteratorFn, options: RunPoolOptions = {}, ) { const results: Promise<{ ok: true; value: K } | { ok: false; error: Error }>[] = [] const activePromises = new Set>() const { shouldPause, shouldCancel } = options for (const item of array) { if (shouldCancel?.()) break if (shouldPause) { await shouldPause() } if (shouldCancel?.()) break const promise = Promise.resolve() .then(() => iteratorFn(item, array)) .then<{ ok: true; value: K }>((value) => ({ ok: true, value })) .catch<{ ok: false; error: Error }>((error) => ({ ok: false, error })) results.push(promise) if (poolLimit < array.length) { activePromises.add(promise) const cleanup = () => activePromises.delete(promise) promise.then(cleanup, cleanup) if (activePromises.size >= poolLimit) { await Promise.race(activePromises) } } } return await Promise.all(results) } export const asyncPool = ( poolLimit: number, array: T[], iteratorFn: IteratorFn, ) => { return runPool(poolLimit, array, iteratorFn) } export const createAsyncPool = ( poolLimit: number, array: T[], iteratorFn: IteratorFn, ) => { let paused = false let cancelled = false let resumeResolve: (() => void) | null = null const controller: PoolController = { pause() { paused = true }, resume() { paused = false resumeResolve?.() resumeResolve = null }, cancel() { cancelled = true resumeResolve?.() resumeResolve = null }, } const shouldPause = async () => { if (paused) { await new Promise((resolve) => (resumeResolve = resolve)) } } const shouldCancel = () => cancelled const run = () => runPool(poolLimit, array, iteratorFn, { shouldPause, shouldCancel }) return { run, controller } } export const getUserAgent = () => { const appSettings = useAppSettingsStore() return appSettings.app.userAgent || APP_TITLE + '/' + APP_VERSION } export const getGitHubApiAuthorization = () => { const appSettings = useAppSettingsStore() return appSettings.app.githubApiToken ? `Bearer ${appSettings.app.githubApiToken}` : '' } // System ScheduledTask Helper export const getTaskSchXmlString = async (delay = 30) => { const { appPath } = useEnvStore().env const xml = /*xml*/ ` ${APP_TITLE} at startup \\${APP_TITLE} true PT${delay}S InteractiveToken HighestAvailable IgnoreNew false false true false false true false true true false false false PT72H 7 ${appPath} tasksch ` return xml } export const setIntervalImmediately = (func: () => void, interval: number) => { func() return setInterval(func, interval) } const isPlainObject = (obj: any) => { return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]' } export const deepAssign = (...args: any[]) => { const len = args.length let target = args[0] if (!isPlainObject(target)) { target = {} } for (let i = 1; i < len; i++) { const source = args[i] if (isPlainObject(source)) { for (const s in source) { if (s === '__proto__' || target === source[s]) { continue } if (isPlainObject(source[s])) { target[s] = deepAssign(target[s], source[s]) } else { target[s] = source[s] } } } } return target } export const readonly = (obj: T): T => { if (typeof obj !== 'object' || obj === null) return obj return new Proxy(obj, { get(target, key) { const result = Reflect.get(target, key) if (typeof result === 'object' && result !== null) { return readonly(result) } return result }, set(target, key) { console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target) return true }, deleteProperty(target, key) { console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target) return true }, defineProperty(target, key) { console.warn( `DefineProperty operation on key "${String(key)}" failed: target is readonly.`, target, ) return false }, setPrototypeOf(target) { console.warn(`SetPrototypeOf operation failed: target is readonly.`, target) return false }, }) } export const normalizeBase64 = (str: string): string => { const normalized = str.trim().replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/') const padding = (4 - (normalized.length % 4)) % 4 return normalized + '='.repeat(padding) } export const base64UrlEncode = (str: string): string => { return base64Encode(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } export const base64Encode = (str: string): string => { const bytes = new TextEncoder().encode(str) const len = bytes.length const chars = Array(len) for (let i = 0; i < len; i++) { chars[i] = String.fromCharCode(bytes[i]!) } return btoa(chars.join('')) } export const base64Decode = (input: string): string => { const base64 = normalizeBase64(input) const binary = atob(base64) const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)) return new TextDecoder().decode(bytes) } export const stringifyNoFolding = (content: any) => { // Disable string folding return stringify(content, { lineWidth: 0, minContentWidth: 0 }) } const regexCache = new Map() export const buildSmartRegExp = (pattern: string, flags = '') => { const key = pattern + '::' + flags if (regexCache.has(key)) return regexCache.get(key)! let r try { r = new RegExp(pattern, flags) } catch { const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') r = new RegExp(escaped, flags) } regexCache.set(key, r) return r } ================================================ FILE: frontend/src/utils/restorer.ts ================================================ import * as Defaults from '@/constant/profile' import { Inbound, Outbound, RuleAction, RulesetType, RuleType as RouteRuleType, DnsServer, } from '@/enums/kernel' import { deepAssign, sampleID } from './others' import { useProfilesStore, useRulesetsStore } from '@/stores' const supportedRuleTypes = [ RouteRuleType.Inbound, RouteRuleType.Network, RouteRuleType.Protocol, RouteRuleType.Domain, RouteRuleType.DomainSuffix, RouteRuleType.DomainKeyword, RouteRuleType.DomainRegex, RouteRuleType.SourceIPCidr, RouteRuleType.IPCidr, RouteRuleType.SourcePort, RouteRuleType.SourcePortRange, RouteRuleType.Port, RouteRuleType.PortRange, RouteRuleType.ProcessName, RouteRuleType.ProcessPath, RouteRuleType.ProcessPathRegex, RouteRuleType.RuleSet, RouteRuleType.IpIsPrivate, RouteRuleType.ClashMode, ] const buildTagIdMapping = (prefix: string, arr?: Recordable[]): Recordable => { if (!arr) return {} return arr.reduce((p, c, i) => ((p[c.tag] = prefix + i), p), {}) } type RestoreProfileOptions = { extraOutboundsIds?: Recordable } export const restoreProfile = ( config: Recordable, name = sampleID(), options: RestoreProfileOptions = {}, ) => { const template = useProfilesStore().getProfileTemplate() const { extraOutboundsIds } = options const InboundsIds = buildTagIdMapping('in-', config.inbounds) const OutboundsIds = buildTagIdMapping('out-', config.outbounds) const RouteRuleSetIds = buildTagIdMapping('ruleset-', config.route?.rule_set) const DnsServersIds = buildTagIdMapping('dns-', config.dns?.servers) extraOutboundsIds && deepAssign(OutboundsIds, extraOutboundsIds) const profile: IProfile = { id: sampleID(), name, log: deepAssign(Defaults.DefaultLog(), config.log), experimental: restoreExperimental(config.experimental, OutboundsIds), inbounds: restoreInbounds(config.inbounds || [], InboundsIds), outbounds: restoreOutbounds(config.outbounds || [], OutboundsIds), route: { rule_set: restoreRouteRuleset(config.route?.rule_set || [], RouteRuleSetIds, OutboundsIds), rules: restoreRouteRules( config.route?.rules || [], InboundsIds, OutboundsIds, RouteRuleSetIds, DnsServersIds, ), auto_detect_interface: config.route?.auto_detect_interface ?? template.route.auto_detect_interface, find_process: config.route?.find_process ?? template.route.find_process, default_interface: config.route?.default_interface ?? template.route.default_interface, final: OutboundsIds[config.route?.final] ?? template.route.final, default_domain_resolver: { server: DnsServersIds[config.route?.default_domain_resolver?.server] ?? template.route.default_domain_resolver.server, client_subnet: config.route?.default_domain_resolver?.client_subnet ?? template.route.default_domain_resolver.client_subnet, }, }, dns: { disable_cache: config.dns?.disable_cache ?? template.dns.disable_cache, disable_expire: config.dns?.disable_expire ?? template.dns.disable_expire, independent_cache: config.dns?.independent_cache ?? template.dns.independent_cache, final: DnsServersIds[config.dns?.final] ?? template.dns.final, strategy: config.dns?.strategy ?? template.dns.strategy, client_subnet: config.dns?.client_subnet ?? template.dns.client_subnet, servers: restoreDnsServers(config.dns?.servers || [], DnsServersIds, OutboundsIds), rules: restoreDnsRules(config.dns?.rules || [], InboundsIds, RouteRuleSetIds, DnsServersIds), }, mixin: Defaults.DefaultMixin(), script: Defaults.DefaultScript(), } return profile } const restoreExperimental = (raw: Recordable, OutboundsIds: Recordable): IExperimental => { const template = Defaults.DefaultExperimental() const experimental = deepAssign(template, raw) experimental.clash_api.external_ui_download_detour = OutboundsIds[template.clash_api.external_ui_download_detour] return experimental } const restoreInbounds = (inbounds: Recordable[], InboundsIds: Recordable): IInbound[] => { return inbounds.flatMap((raw) => { if (![Inbound.Mixed, Inbound.Http, Inbound.Socks, Inbound.Tun].includes(raw.type)) return [] const inbound: IInbound = { id: InboundsIds[raw.tag], tag: raw.tag, type: raw.type, enable: true, } if (raw.type === Inbound.Tun) { const template = Defaults.DefaultInboundTun() inbound.tun = { interface_name: raw.interface_name ?? template.interface_name, address: raw.address ?? template.address, mtu: raw.mtu ?? template.mtu, auto_route: raw.auto_route ?? template.auto_route, strict_route: raw.strict_route ?? template.strict_route, route_address: raw.route_address ?? template.route_address, route_exclude_address: raw.route_exclude_address ?? template.route_exclude_address, endpoint_independent_nat: raw.endpoint_independent_nat ?? template.endpoint_independent_nat, stack: raw.stack ?? template.stack, } } if ([Inbound.Mixed, Inbound.Http, Inbound.Socks].includes(raw.type)) { const template = Defaults.DefaultInboundMixed() inbound[raw.type as Exclude] = { listen: { listen: raw.listen ?? template.listen.listen, listen_port: raw.listen_port ?? template.listen.listen_port, tcp_fast_open: raw.tcp_fast_open ?? template.listen.tcp_fast_open, tcp_multi_path: raw.tcp_multi_path ?? template.listen.tcp_multi_path, udp_fragment: raw.udp_fragment ?? template.listen.udp_fragment, }, users: raw.users?.map((user: any) => user.username + ':' + user.password) ?? template.users, } } return inbound }) } const restoreOutbounds = (outbounds: Recordable[], OutboundsIds: Recordable): IOutbound[] => { return outbounds.flatMap((raw) => { if (![Outbound.Selector, Outbound.Urltest].includes(raw.type)) { return [] } const outbound = Defaults.DefaultOutbound() outbound.id = OutboundsIds[raw.tag] outbound.tag = raw.tag outbound.type = raw.type if ([Outbound.Selector, Outbound.Urltest].includes(raw.type)) { if ('interrupt_exist_connections' in raw) { outbound.interrupt_exist_connections = raw.interrupt_exist_connections } outbound.outbounds = raw.outbounds?.flatMap((tag: string) => { if (!OutboundsIds[tag]) return [] const isBuiltIn = [Outbound.Direct, Outbound.Block].includes(tag as Outbound) return { id: isBuiltIn ? tag : OutboundsIds[tag], type: 'Built-in', tag, } }) } if (Outbound.Urltest === raw.type) { if ('url' in raw) { outbound.url = raw.url } if ('interval' in raw) { outbound.interval = raw.interval } if ('tolerance' in raw) { outbound.tolerance = raw.tolerance } } return outbound }) } const restoreRouteRuleset = ( rulesets: Recordable[], RouteRuleSetIds: Recordable, OutboundsIds: Recordable, ): IRuleSet[] => { const rulesetsStore = useRulesetsStore() return rulesets.flatMap((raw) => { const ruleset = Defaults.DefaultRouteRuleset() ruleset.id = RouteRuleSetIds[raw.tag] ruleset.type = raw.type ruleset.tag = raw.tag if (raw.type === RulesetType.Inline) { if ('rules' in raw) { ruleset.rules = JSON.stringify(raw.rules, null, 2) } } else if (raw.type === RulesetType.Local) { if ('path' in raw) { const r = rulesetsStore.rulesets.find((v) => v.path === raw.path.replace('../', 'data/')) if (r) { ruleset.path = r.id } else { ruleset.path = raw.path } } if ('format' in raw) { ruleset.format = raw.format } } else if (raw.type === RulesetType.Remote) { if ('format' in raw) { ruleset.format = raw.format } if ('url' in raw) { ruleset.url = raw.url } if ('download_detour' in raw) { ruleset.download_detour = OutboundsIds[raw.download_detour] } if ('update_interval' in raw) { ruleset.update_interval = raw.update_interval } } return ruleset }) } const restoreRouteRules = ( rules: Recordable[], InboundsIds: Recordable, OutboundsIds: Recordable, RouteRuleSetIds: Recordable, DnsServersIds: Recordable, ): IRule[] => { return rules.flatMap((raw, i) => { const rule = Defaults.DefaultRouteRule() rule.id = 'rule-' + i rule.action = raw.action || RuleAction.Route const hits = supportedRuleTypes.filter((key) => key in raw) if (hits.length === 1) { rule.type = hits[0] as any } else { rule.type = RouteRuleType.Inline } if (rule.type === RouteRuleType.Inline) { rule.payload = JSON.stringify( { ...raw, action: undefined, invert: undefined, outbound: undefined, sniffer: undefined, strategy: undefined, server: undefined, }, null, 2, ) } else if (rule.type === RouteRuleType.Inbound) { rule.payload = InboundsIds[raw[rule.type]] } else if (rule.type === RouteRuleType.RuleSet) { rule.payload = raw[rule.type].map((tag: string) => RouteRuleSetIds[tag]).join(',') } else { rule.payload = String(raw[rule.type]) } if (RuleAction.Route === raw.action) { rule.outbound = OutboundsIds[raw.outbound] } else if (RuleAction.Reject === raw.action) { if ('method' in raw) { rule.outbound = raw.method } } else if (RuleAction.RouteOptions === raw.action) { rule.outbound = JSON.stringify( { ...raw, action: undefined, invert: undefined, ...supportedRuleTypes.reduce((p, c) => ((p[c] = undefined), p), {} as Recordable), }, null, 2, ) } else if (RuleAction.Sniff === raw.action) { if ('sniffer' in raw) { rule.sniffer = Array.isArray(raw.sniffer) ? raw.sniffer : [raw.sniffer] } } else if (RuleAction.Resolve === raw.action) { if ('strategy' in raw) { rule.strategy = raw.strategy } if ('server' in raw) { rule.server = DnsServersIds[raw.server] } } if ('invert' in raw) { rule.invert = raw.invert } return rule }) } const restoreDnsServers = ( servers: Recordable[], DnsServersIds: Recordable, OutboundsIds: Recordable, ): IDNSServer[] => { return servers.flatMap((raw) => { if (!raw.type) return [] const server = Defaults.DefaultDnsServer() server.id = DnsServersIds[raw.tag] server.tag = raw.tag server.type = raw.type if ( [ DnsServer.Local, DnsServer.Tcp, DnsServer.Udp, DnsServer.Tls, DnsServer.Quic, DnsServer.Https, DnsServer.H3, DnsServer.Dhcp, ].includes(raw.type) ) { if ('detour' in raw) { server.detour = OutboundsIds[raw.detour] } if ('domain_resolver' in raw) { server.domain_resolver = DnsServersIds[raw.domain_resolver] } if ( [ DnsServer.Tcp, DnsServer.Udp, DnsServer.Tls, DnsServer.Quic, DnsServer.Https, DnsServer.H3, ].includes(raw.type) ) { if ('server' in raw) { server.server = raw.server } if ('server_port' in raw) { server.server_port = raw.server_port } if ([DnsServer.Https, DnsServer.H3].includes(raw.type)) { if ('path' in raw) { server.path = raw.path } } } } else if (DnsServer.Hosts === server.type) { if ('path' in raw) { server.hosts_path = raw.path } if ('predefined' in raw) { server.predefined = Object.entries(raw.predefined).reduce( (p, [key, value]) => { p[key] = Array.isArray(value) ? value.join(',') : value return p }, {} as Recordable, ) } } else if (DnsServer.Dhcp === server.type) { if ('interface' in raw) { server.interface = raw.interface } } else if (DnsServer.FakeIP === server.type) { if ('inet4_range' in raw) { server.inet4_range = raw.inet4_range } if ('inet6_range' in raw) { server.inet6_range = raw.inet6_range } } return server }) } const restoreDnsRules = ( rules: Recordable[], InboundsIds: Recordable, RouteRuleSetIds: Recordable, DnsServersIds: Recordable, ): IDNSRule[] => { return rules.flatMap((raw: Recordable, i) => { const rule = Defaults.DefaultDnsRule() rule.id = 'rule-' + i rule.action = raw.action || RuleAction.Route const hits = supportedRuleTypes.filter((key) => key in raw) if (hits.length === 1) { rule.type = hits[0] as any } else { rule.type = RouteRuleType.Inline } if (rule.type === RouteRuleType.Inline) { rule.payload = JSON.stringify( { ...raw, action: undefined, invert: undefined, client_subnet: undefined, disable_cache: undefined, strategy: undefined, server: undefined, }, null, 2, ) } else if (rule.type === RouteRuleType.Inbound) { rule.payload = InboundsIds[raw[rule.type]] } else if (rule.type === RouteRuleType.RuleSet) { rule.payload = raw[rule.type].map((tag: string) => RouteRuleSetIds[tag]).join(',') } else { rule.payload = raw[rule.type] } if (RuleAction.Route === raw.action) { if ('server' in raw) { rule.server = DnsServersIds[raw.server] } if ('strategy' in raw) { rule.strategy = raw.strategy } } else if (RuleAction.Reject === raw.action) { if ('method' in raw) { rule.server = raw.method } } else if ([RuleAction.RouteOptions, RuleAction.Predefined].includes(raw.action)) { rule.server = JSON.stringify( { ...raw, action: undefined, invert: undefined, disable_cache: undefined, ...supportedRuleTypes.reduce((p, c) => ((p[c] = undefined), p), {} as Recordable), }, null, 2, ) } if ([RuleAction.Route, RuleAction.RouteOptions].includes(raw.action)) { if ('disable_cache' in raw) { rule.disable_cache = raw.disable_cache } if ('client_subnet' in raw) { rule.client_subnet = raw.client_subnet } } if ('invert' in raw) { rule.invert = raw.invert } return rule }) } ================================================ FILE: frontend/src/utils/tray.ts ================================================ import { Notify, RestartApp, EventsOn, EventsOff, ShowMainWindow, UpdateTrayAndMenus, } from '@/bridge' import { ColorOptions, ThemeOptions } from '@/constant/app' import { ModeOptions } from '@/constant/kernel' import i18n from '@/lang' import { useAppSettingsStore, useKernelApiStore, useEnvStore, usePluginsStore, useAppStore, } from '@/stores' import { debounce, exitApp, handleChangeMode, APP_TITLE, APP_VERSION, handleUseProxy, } from '@/utils' import type { MenuItem } from '@/types/app' const getTrayIcons = () => { const envStore = useEnvStore() const appSettings = useAppSettingsStore() const kernelApiStore = useKernelApiStore() const themeMode = appSettings.themeMode const ext = envStore.env.os === 'linux' ? '.png' : '.ico' const folder = envStore.env.os === 'linux' ? 'imgs' : 'icons' let icon = `data/.cache/${folder}/tray_normal_${themeMode}${ext}` if (kernelApiStore.running) { if (kernelApiStore.config.tun.enable) { icon = `data/.cache/${folder}/tray_tun_${themeMode}${ext}` } else if (envStore.systemProxy) { icon = `data/.cache/${folder}/tray_proxy_${themeMode}${ext}` } } return icon } const generateUniqueEventsForMenu = (menus: MenuItem[]) => { const { t } = i18n.global const MenuItemHandlerMap: Recordable<() => void> = {} EventsOff('onMenuItemClick') EventsOn('onMenuItemClick', (id) => MenuItemHandlerMap[id]?.()) let index = 0 function processMenu(menu: MenuItem) { const _menu = { ...menu, text: t(menu.text || ''), tooltip: t(menu.tooltip || '') } const { event, children } = menu if (event) { _menu.event = index + '_' + menu.text MenuItemHandlerMap[_menu.event] = event as any } if (children && children.length > 0) { _menu.children = children.map(processMenu) } index += 1 return _menu } return menus.map(processMenu) } const getTrayMenus = () => { const appStore = useAppStore() const envStore = useEnvStore() const appSettings = useAppSettingsStore() const kernelApiStore = useKernelApiStore() const pluginsStore = usePluginsStore() let pluginMenus: MenuItem[] = [] let pluginMenusHidden = !appSettings.app.addPluginToMenu let groupMenus: MenuItem[] = [] const groupMenusHidden = !appSettings.app.addGroupToMenu if (!groupMenusHidden) { const { proxies } = kernelApiStore if (!proxies) return [] groupMenus = Object.values(proxies) .filter((v) => ['Selector', 'URLTest'].includes(v.type) && v.name !== 'GLOBAL') .concat(proxies.GLOBAL || []) .map((group) => { const all = (group.all || []) .filter((proxy) => { const history = proxies[proxy]?.history || [] const alive = (history[history.length - 1]?.delay || 0) > 0 return ( appSettings.app.kernel.unAvailable || ['direct', 'block'].includes(proxy) || proxies[proxy]?.all || alive ) }) .map((proxy) => { const history = proxies[proxy]?.history || [] const delay = history[history.length - 1]?.delay || 0 return { ...proxies[proxy], delay } }) .sort((a, b) => { if (!appSettings.app.kernel.sortByDelay || a.delay === b.delay) return 0 if (!a.delay) return 1 if (!b.delay) return -1 return a.delay - b.delay }) return { ...group, all } }) .map((group) => { return { type: 'item', text: group.name, show: true, children: group.all.map((proxy) => { return { type: 'item', text: proxy.name, show: true, checked: proxy.name === group.now, event: () => { handleUseProxy(group, proxy) }, } }), } }) } if (!pluginMenusHidden) { const filtered = pluginsStore.plugins.filter( (plugin) => Object.keys(plugin.menus).length && !plugin.disabled, ) pluginMenusHidden = filtered.length === 0 pluginMenus = filtered.map(({ id, name, menus }) => { return { type: 'item', text: name, children: Object.entries(menus).map(([text, event]) => { return { type: 'item', text, event: () => { pluginsStore.manualTrigger(id, event as any).catch((err: any) => { Notify('Error', err.message || err) }) }, } }), } }) } const trayMenus: MenuItem[] = [ { type: 'item', text: 'tray.showMainWindow', hidden: envStore.env.os === 'windows', event: ShowMainWindow, }, { type: 'separator', hidden: envStore.env.os === 'windows', }, { type: 'item', text: 'kernel.mode', hidden: !kernelApiStore.running, children: ModeOptions.map((mode) => ({ type: 'item', text: mode.label, checked: kernelApiStore.config.mode === mode.value, event: () => handleChangeMode(mode.value), })), }, { type: 'item', text: 'tray.proxyGroup', hidden: groupMenusHidden || !kernelApiStore.running, children: groupMenus, }, { type: 'item', text: 'tray.kernel', children: [ { type: 'item', text: 'tray.startKernel', hidden: kernelApiStore.running, event: kernelApiStore.startCore, }, { type: 'item', text: 'tray.restartKernel', hidden: !kernelApiStore.running, event: kernelApiStore.restartCore, }, { type: 'item', text: 'tray.stopKernel', hidden: !kernelApiStore.running, event: kernelApiStore.stopCore, }, ], }, { type: 'separator', hidden: !kernelApiStore.running, }, { type: 'item', text: 'tray.proxy', hidden: !kernelApiStore.running, children: [ { type: 'item', text: 'tray.setSystemProxy', hidden: envStore.systemProxy, event: envStore.setSystemProxy, }, { type: 'item', text: 'tray.clearSystemProxy', hidden: !envStore.systemProxy, event: envStore.clearSystemProxy, }, ], }, { type: 'item', text: 'tray.tun', hidden: !kernelApiStore.running, children: [ { type: 'item', text: 'tray.enableTunMode', hidden: kernelApiStore.config.tun.enable, event: () => kernelApiStore.updateConfig('tun', { enable: true }), }, { type: 'item', text: 'tray.disableTunMode', hidden: !kernelApiStore.config.tun.enable, event: () => kernelApiStore.updateConfig('tun', { enable: false }), }, ], }, { type: 'item', text: 'settings.general', children: [ { type: 'item', text: 'settings.theme.name', children: ThemeOptions.map((theme) => ({ type: 'item', text: theme.label, checked: appSettings.app.theme === theme.value, event: () => (appSettings.app.theme = theme.value), })), }, { type: 'item', text: 'settings.color.name', children: ColorOptions.map((color) => ({ type: 'item', text: color.label, checked: appSettings.app.color === color.value, event: () => (appSettings.app.color = color.value), })), }, { type: 'item', text: 'settings.lang.name', children: appStore.locales.map((v) => ({ type: 'item', text: v.label, checked: appSettings.app.lang === v.value, event: () => (appSettings.app.lang = v.value), })), }, ], }, { type: 'item', text: 'tray.plugins', hidden: pluginMenusHidden, children: pluginMenus, }, { type: 'separator', }, { type: 'item', text: 'tray.restart', tooltip: 'tray.restartTip', event: RestartApp, }, { type: 'item', text: 'tray.exit', tooltip: 'tray.exitTip', event: exitApp, }, ] return trayMenus } export const updateTrayAndMenus = debounce(async () => { const trayMenus = getTrayMenus() const trayIcons = getTrayIcons() const pluginsStore = usePluginsStore() const isDarwin = useEnvStore().env.os === 'darwin' const title = isDarwin ? '' : APP_TITLE const tray = { icon: trayIcons, title, tooltip: APP_TITLE + ' ' + APP_VERSION } const [finalTray, finalMenus] = await pluginsStore.onTrayUpdateTrigger(tray, trayMenus) await UpdateTrayAndMenus(finalTray, generateUniqueEventsForMenu(finalMenus) as any) }, 500) ================================================ FILE: frontend/src/views/HomeView/components/CommonController.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/components/ConnectionsController.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/components/GroupsController.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/components/KernelLogs.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/components/LogsController.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/components/OverView.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/components/QuickStart.vue ================================================ ================================================ FILE: frontend/src/views/HomeView/index.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/components/PluginChangelog.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/components/PluginConfigItem.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/components/PluginConfigurator.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/components/PluginForm.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/components/PluginHub.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/components/PluginView.vue ================================================ ================================================ FILE: frontend/src/views/PluginsView/index.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/DnsConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/DnsRulesConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/DnsServersConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/GeneralConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/InboundsConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/MixinAndScriptConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/OutboundsConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/ProfileEditor.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/ProfileForm.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/RouteConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/RouteRulesConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/components/RouteRulesetConfig.vue ================================================ ================================================ FILE: frontend/src/views/ProfilesView/index.vue ================================================ ================================================ FILE: frontend/src/views/RulesetsView/components/RulesetForm.vue ================================================ ================================================ FILE: frontend/src/views/RulesetsView/components/RulesetHub.vue ================================================ ================================================ FILE: frontend/src/views/RulesetsView/components/RulesetView.vue ================================================ ================================================ FILE: frontend/src/views/RulesetsView/index.vue ================================================ ================================================ FILE: frontend/src/views/ScheduledTasksView/components/ScheduledTaskForm.vue ================================================ ================================================ FILE: frontend/src/views/ScheduledTasksView/components/ScheduledTasksLogs.vue ================================================ ================================================ FILE: frontend/src/views/ScheduledTasksView/index.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/CoreSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/GeneralSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/PluginSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/AdvancedSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/BehaviorSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/BranchDetail.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/CoreConfig.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/FeatureSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/PersonalizationSettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/SwitchBranch.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/components/components/SystemProxySettings.vue ================================================ ================================================ FILE: frontend/src/views/SettingsView/index.vue ================================================ ================================================ FILE: frontend/src/views/SubscribesView/components/ProxiesEditor.vue ================================================ ================================================ FILE: frontend/src/views/SubscribesView/components/ProxiesView.vue ================================================ ================================================ FILE: frontend/src/views/SubscribesView/components/SubscribeForm.vue ================================================ ================================================ FILE: frontend/src/views/SubscribesView/components/SubscribeScript.vue ================================================ ================================================ FILE: frontend/src/views/SubscribesView/index.vue ================================================ ================================================ FILE: frontend/tsconfig.app.json ================================================ { "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "baseUrl": ".", "lib": ["ESNext", "DOM"], "paths": { "@/*": ["./src/*"], "@wails/*": ["./src/bridge/wailsjs/*"] } } } ================================================ FILE: frontend/tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.app.json" } ] } ================================================ FILE: frontend/tsconfig.node.json ================================================ { "extends": "@tsconfig/node24/tsconfig.json", "include": ["vite.config.*", "eslint.config.*"], "compilerOptions": { "composite": true, "noEmit": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "module": "ESNext", "moduleResolution": "Bundler", "types": ["node"] } } ================================================ FILE: frontend/vite.config.ts ================================================ import { fileURLToPath, URL } from 'node:url' import vue from '@vitejs/plugin-vue' import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ base: './', plugins: [vue()], resolve: { extensions: ['.ts', '.js'], alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), '@wails': fileURLToPath(new URL('./src/bridge/wailsjs', import.meta.url)), vue: 'vue/dist/vue.esm-bundler.js', }, }, build: { cssCodeSplit: false, chunkSizeWarningLimit: 4096, // 4MB rolldownOptions: { output: { codeSplitting: { groups: [ { name: 'vue', test: /node_modules\/vue/ }, { name: 'codemirror', test: /node_modules\/@codemirror/ }, { name: 'prettier', test: /node_modules\/prettier/ }, { name: 'vendor', test: /node_modules/ }, { name: 'index' }, ], }, }, }, }, }) ================================================ FILE: go.mod ================================================ module guiforcores go 1.26 require ( github.com/energye/systray v1.0.3 github.com/gen2brain/beeep v0.11.2 github.com/oschwald/geoip2-golang v1.13.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/shirou/gopsutil/v3 v3.24.5 github.com/wailsapp/wails/v2 v2.11.0 golang.org/x/sys v0.41.0 golang.org/x/text v0.34.0 gopkg.in/yaml.v3 v3.0.1 ) require ( git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/esiqveland/notify v0.13.3 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jackmordaunt/icns/v3 v3.0.1 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/labstack/echo/v4 v4.15.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/gosod v1.0.4 // indirect github.com/leaanthony/slicer v1.6.0 // indirect github.com/leaanthony/u v1.1.1 // indirect github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/oschwald/maxminddb-golang v1.13.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/samber/lo v1.52.0 // indirect github.com/sergeymakinen/go-bmp v1.0.0 // indirect github.com/sergeymakinen/go-ico v1.0.0 // indirect github.com/shoenig/go-m1cpu v0.1.7 // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) replace github.com/energye/systray => github.com/GUI-for-Cores/systray v1.0.1 ================================================ FILE: go.sum ================================================ git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE= git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo= github.com/GUI-for-Cores/systray v1.0.1 h1:nJEIsm3yHoYhQD7PnUxCD2bOP/+axLt/bWLWHB8OWT8= github.com/GUI-for-Cores/systray v1.0.1/go.mod h1:HelKhC3PXwv3ryDxbuQqV+7kAxAYNzE5cfdrerGOZTc= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o= github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE= github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA= github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o= github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M= github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY= github.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80= github.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0= github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: main.go ================================================ package main import ( "context" "embed" "guiforcores/bridge" "time" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/logger" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/linux" "github.com/wailsapp/wails/v2/pkg/options/mac" "github.com/wailsapp/wails/v2/pkg/options/windows" "github.com/wailsapp/wails/v2/pkg/runtime" ) //go:embed all:frontend/dist var assets embed.FS //go:embed frontend/dist/favicon.ico var icon []byte func main() { app := bridge.CreateApp(assets) trayStart, trayEnd := bridge.CreateTray(app, icon) // Create application with options err := wails.Run(&options.App{ MinWidth: 600, MinHeight: 400, DisableResize: false, Menu: app.AppMenu, Title: bridge.Env.AppName, Frameless: bridge.Env.OS != "darwin", Width: bridge.Config.Width, Height: bridge.Config.Height, StartHidden: bridge.Config.StartHidden, WindowStartState: options.WindowStartState(bridge.Config.WindowStartState), BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1}, Windows: &windows.Options{ WebviewIsTransparent: true, WindowIsTranslucent: true, BackdropType: windows.Acrylic, WebviewBrowserPath: bridge.Env.WebviewPath, }, Mac: &mac.Options{ TitleBar: mac.TitleBarHiddenInset(), Appearance: mac.DefaultAppearance, WebviewIsTransparent: true, WindowIsTranslucent: true, About: &mac.AboutInfo{ Title: bridge.Env.AppName, Message: "© 2026 GUI.for.Cores", Icon: icon, }, }, Linux: &linux.Options{ Icon: icon, WindowIsTranslucent: false, ProgramName: bridge.Env.AppName, WebviewGpuPolicy: linux.WebviewGpuPolicy(bridge.Config.WebviewGpuPolicy), }, AssetServer: &assetserver.Options{ Assets: assets, Middleware: bridge.RollingRelease, }, SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: func() string { if bridge.Config.MultipleInstance { return time.Now().String() } return bridge.Env.AppName }(), OnSecondInstanceLaunch: func(data options.SecondInstanceData) { runtime.Show(app.Ctx) runtime.EventsEmit(app.Ctx, "onLaunchApp", data.Args) }, }, OnStartup: func(ctx context.Context) { app.Ctx = ctx trayStart() }, OnBeforeClose: func(ctx context.Context) (prevent bool) { if !bridge.Env.PreventExit { trayEnd() return false } runtime.EventsEmit(ctx, "onBeforeExitApp") return true }, Bind: []any{ app, }, LogLevel: logger.INFO, Debug: options.Debug{ OpenInspectorOnStartup: true, }, }) if err != nil { println("Error:", err.Error()) } } ================================================ FILE: wails.json ================================================ { "$schema": "https://wails.io/schemas/config.v2.json", "name": "GUI.for.SingBox", "outputfilename": "GUI.for.SingBox", "frontend:install": "pnpm install", "frontend:build": "pnpm run build", "frontend:dev:watcher": "pnpm run dev", "frontend:dev:serverUrl": "auto", "wailsjsdir": "frontend/src/bridge", "author": { "name": "GUI.for.Cores", "email": "GUI.for.Cores@github.com" }, "info": { "copyright": "Copyright", "comments": "https://github.com/GUI-for-Cores" } }