mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-16 10:55:37 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a45d1156 | |||
| 4e651a72f7 | |||
| 7c563153ae | |||
| cb81c0df85 | |||
| 9c316ea01c | |||
| 541fc664e3 | |||
| 18478b7c4b | |||
| 650323faef | |||
| ed131272d4 | |||
| 39b056c87a | |||
| c19cd1bff3 | |||
| 37531507db | |||
| ca9b4c58b1 | |||
| 4341bcba5d | |||
| 0be4ac1fa5 | |||
| 28cd6da502 | |||
| 0712ef762d | |||
| eee7d7a1ed | |||
| 4c58def0db | |||
| c6a32e4467 | |||
| 30f0ff16ca | |||
| 38d117ee44 | |||
| 7aba65ea32 | |||
| fe4dff5df0 | |||
| 2bc51daa98 | |||
| 838b6101b9 | |||
| 056c9da781 | |||
| 2a656d6a0c | |||
| 43a650f9ab | |||
| 88a55859ac | |||
| d686c8721f | |||
| 0a718163fd | |||
| 53f279f5ff | |||
| ae6d929f4a | |||
| bb82b3a5b0 | |||
| 70b122fb91 | |||
| 67cba2c326 | |||
| b86692d009 | |||
| 28e645a277 | |||
| 1f2517c731 | |||
| b44053f496 | |||
| 5b9ac65477 | |||
| d726d46a00 | |||
| 1273426009 | |||
| b50744690e | |||
| 55b93454dc | |||
| 89cc75f674 | |||
| 6bb2fd9a15 | |||
| 8ab98bba8f | |||
| 26d002bc2b | |||
| 71679e889a | |||
| 7485f5f64e | |||
| bbe8f9f810 | |||
| eba9504fc2 | |||
| 67ac9b00ff | |||
| 3ffa6214ca | |||
| 6f278ab167 | |||
| f10b45a67c | |||
| cc8f35787e | |||
| 8f1786fa23 | |||
| 70dddeace3 | |||
| 8cc9da9d6d | |||
| 5292b87275 | |||
| 87b7b7ed7c | |||
| 999a486928 | |||
| 627e989faa | |||
| af95312949 | |||
| a452c34390 | |||
| 4d5330fa0a | |||
| 5e48626cb9 | |||
| ad7dc3a129 | |||
| 92fab5aafa | |||
| 841d525913 | |||
| d2efbbef04 | |||
| 971ef82679 | |||
| 020bf04ec4 | |||
| 4d91582fd8 | |||
| e9b4dbce6e | |||
| 00fd02c739 | |||
| c0d2045e52 | |||
| 835cd407bf | |||
| f5ba5bb146 | |||
| 7a694257d9 | |||
| 67abf4446d | |||
| 7035a3fef4 | |||
| 4445916ba7 | |||
| a102a8bfc7 | |||
| c9e8c35e77 |
@@ -160,7 +160,7 @@ jobs:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
|
||||
cache-targets: "false"
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
elif [[ $TARGET =~ ^riscv64.*$ || $TARGET =~ ^loongarch64.*$ ]]; then
|
||||
elif [[ $TARGET =~ ^riscv64.*$ || $TARGET =~ ^loongarch64.*$ || $TARGET =~ ^aarch64.*$ ]]; then
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
else
|
||||
CORE_FEATURES="--features=jemalloc"
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
uses: vmactions/freebsd-vm@670398e4236735b8b65805c3da44b7a511fb8b27
|
||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||
env:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
@@ -245,13 +245,13 @@ jobs:
|
||||
# windows is the only OS using a different convention for executable file name
|
||||
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/*.dll ./artifacts/objects/
|
||||
cp easytier/third_party/x86_64/* ./artifacts/objects/
|
||||
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/i686/*.dll ./artifacts/objects/
|
||||
cp easytier/third_party/i686/* ./artifacts/objects/
|
||||
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/arm64/*.dll ./artifacts/objects/
|
||||
cp easytier/third_party/arm64/* ./artifacts/objects/
|
||||
fi
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.4.4'
|
||||
default: 'v2.5.0'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
sudo apt install aptitude
|
||||
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
|
||||
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
|
||||
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
|
||||
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu libsoup-3.0-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -170,11 +170,11 @@ jobs:
|
||||
if: ${{ matrix.OS == 'windows-latest' }}
|
||||
run: |
|
||||
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
|
||||
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
|
||||
cp ./easytier/third_party/arm64/* ./easytier-gui/src-tauri/
|
||||
elif [[ $GUI_TARGET =~ ^i686.*$ ]]; then
|
||||
cp ./easytier/third_party/i686/*.dll ./easytier-gui/src-tauri/
|
||||
cp ./easytier/third_party/i686/* ./easytier-gui/src-tauri/
|
||||
else
|
||||
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
|
||||
cp ./easytier/third_party/x86_64/* ./easytier-gui/src-tauri/
|
||||
fi
|
||||
|
||||
- name: Build GUI
|
||||
|
||||
+97
-26
@@ -5,6 +5,7 @@ on:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -15,6 +16,16 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
cargo_fmt_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: fmt check
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
bash ../../.github/workflows/install_rust.sh
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all -- --check
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
@@ -27,9 +38,9 @@ jobs:
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
concurrent_skipping: "same_content_newer"
|
||||
skip_after_successful_duplicate: "true"
|
||||
cancel_others: "true"
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-ohos:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -45,38 +56,63 @@ jobs:
|
||||
wget \
|
||||
unzip \
|
||||
git \
|
||||
pkg-config
|
||||
pkg-config curl libgl1-mesa-dev expect
|
||||
sudo apt-get clean
|
||||
|
||||
- name: Download and extract native SDK
|
||||
working-directory: ../../../
|
||||
- name: Count commits since last tag on upstream main
|
||||
run: |
|
||||
echo $PWD
|
||||
wget -q \
|
||||
https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.aa
|
||||
wget -q \
|
||||
https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.ab
|
||||
cat ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab > sdk.tar.gz
|
||||
echo "Extracting native..."
|
||||
mkdir sdk
|
||||
tar -xzf sdk.tar.gz ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip
|
||||
tar -xzf sdk.tar.gz ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip
|
||||
unzip -qq ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip -d sdk
|
||||
unzip -qq ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip -d sdk
|
||||
ls -la sdk/native/llvm/bin/
|
||||
rm -rf ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab ohos-sdk/
|
||||
UPSTREAM_REPO="https://github.com/EasyTier/EasyTier.git"
|
||||
|
||||
git remote add upstream "$UPSTREAM_REPO" 2>/dev/null || true
|
||||
git fetch upstream --tags --force
|
||||
|
||||
# 获取 upstream/main 最新提交
|
||||
git fetch upstream main
|
||||
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 upstream/main 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$LAST_TAG" ]; then
|
||||
DIFF_COUNT=$(git rev-list --count upstream/main)
|
||||
else
|
||||
DIFF_COUNT=$(git rev-list --count "${LAST_TAG}..upstream/main")
|
||||
fi
|
||||
|
||||
echo "TAG_COMMIT_DIFF=$DIFF_COUNT"
|
||||
echo "TAG_COMMIT_DIFF=$DIFF_COUNT" >> $GITHUB_ENV
|
||||
|
||||
- name: Get easytier version
|
||||
run: |
|
||||
EASYTIER_CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps --manifest-path easytier/Cargo.toml \
|
||||
| jq -r '.packages[0].version')
|
||||
EASYTIER_VERSION="${EASYTIER_CARGO_VERSION}-${TAG_COMMIT_DIFF}"
|
||||
echo "EASYTIER_VERSION=${EASYTIER_VERSION}" >> $GITHUB_ENV
|
||||
cd ./easytier-contrib/easytier-ohrs/package
|
||||
jq --arg v "$EASYTIER_VERSION" '.version = $v' oh-package.json5 > oh-package.tmp.json5
|
||||
mv oh-package.tmp.json5 oh-package.json5
|
||||
|
||||
- name: Generate CHANGELOG.md for current commit
|
||||
working-directory: ./easytier-contrib/easytier-ohrs/package
|
||||
run: |
|
||||
{
|
||||
echo "## easytier-ohrs ${EASYTIER_VERSION}"
|
||||
echo
|
||||
git log -1 --pretty=format:"- %s"
|
||||
echo
|
||||
} > CHANGELOG.md
|
||||
|
||||
- name: Setup HarmonyOS CLI tools
|
||||
uses: ErBWs/setup-ohos@v1
|
||||
|
||||
- name: Download and Extract Custom SDK
|
||||
run: |
|
||||
wget https://github.com/FrankHan052176/Easytier-OHOS-sdk/releases/download/v1/ohos-sdk.zip -O /tmp/ohos-sdk.zip
|
||||
sudo unzip -o /tmp/ohos-sdk.zip -d /tmp/custom-sdk
|
||||
sudo cp -rf /tmp/custom-sdk/linux/native/* $HOME/sdk/native
|
||||
echo "Custom SDK files deployed to $HOME/sdk/native"
|
||||
ls -a $HOME/sdk/native
|
||||
sudo cp -rf /tmp/custom-sdk/linux/native/* $OHOS_NDK_HOME/native
|
||||
echo "Custom SDK files deployed to $OHOS_NDK_HOME/native"
|
||||
ls -a $OHOS_NDK_HOME/native
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
echo "OHOS_NDK_HOME=$HOME/sdk" >> $GITHUB_ENV
|
||||
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
|
||||
|
||||
- name: Create clang wrapper script
|
||||
@@ -104,11 +140,46 @@ jobs:
|
||||
cargo update easytier
|
||||
ohrs doctor
|
||||
ohrs build --release --arch aarch
|
||||
|
||||
ohrs artifact
|
||||
mv package.har easytier-ohrs.har
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: easytier-ohos
|
||||
path: ./easytier-contrib/easytier-ohrs/dist/arm64-v8a/libeasytier_ohrs.so
|
||||
path: |
|
||||
./easytier-contrib/easytier-ohrs/easytier-ohrs.har
|
||||
./easytier-contrib/easytier-ohrs/dist/arm64-v8a/libeasytier_ohrs.so
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish To Center Ohpm
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
env:
|
||||
OHPM_PUBLISH_CODE: ${{ secrets.OHPM_PUBLISH_CODE }}
|
||||
OHPM_PRIVATE_KEY: ${{ secrets.OHPM_PRIVATE_KEY }}
|
||||
OHPM_KEY_PASSPHRASE: ${{ secrets.OHPM_KEY_PASSPHRASE }}
|
||||
run: |
|
||||
ohpm config set publish_id "$OHPM_PUBLISH_CODE"
|
||||
ohpm config set publish_registry https://ohpm.openharmony.cn/ohpm
|
||||
TMP_DIR=$(mktemp -d)
|
||||
PRIVATE_KEY_FILE="$TMP_DIR/private_key"
|
||||
printf '%s' "$OHPM_PRIVATE_KEY" > "$PRIVATE_KEY_FILE"
|
||||
chmod 600 "$PRIVATE_KEY_FILE"
|
||||
ohpm config set key_path $PRIVATE_KEY_FILE
|
||||
unzip ohpm_crypto.zip -d /home/runner/work/
|
||||
ohpm config set crypto_path /home/runner/work/ohpm_crypto
|
||||
chmod 755 /home/runner/work/ohpm_crypto/*
|
||||
PASSPHRASE="$(printf '%s' "$OHPM_KEY_PASSPHRASE" | tr -d '\r\n')"
|
||||
ohpm config set key_passphrase "$PASSPHRASE"
|
||||
ohpm publish easytier-ohrs.har
|
||||
|
||||
- name: Publish To Private Ohpm
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
printf '%s' "${{ secrets.CODEARTS_PRIVATE_OHPM }}" > ~/.ohpm/.ohpmrc
|
||||
ohpm config set strict_ssl false
|
||||
ohpm publish easytier-ohrs.har
|
||||
curl --header "Content-Type: application/json" --request POST --data "{}" ${{ secrets.CODEARTS_WEBHOOKS }}
|
||||
|
||||
@@ -6,22 +6,19 @@ on:
|
||||
core_run_id:
|
||||
description: 'The run id of EasyTier-Core Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498549
|
||||
required: true
|
||||
gui_run_id:
|
||||
description: 'The run id of EasyTier-GUI Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498557
|
||||
required: true
|
||||
mobile_run_id:
|
||||
description: 'The run id of EasyTier-Mobile Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498555
|
||||
required: true
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.4.4'
|
||||
default: 'v2.5.0'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -34,7 +31,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: contains('["KKRainbow"]', github.actor)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
@@ -46,7 +42,7 @@ jobs:
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.core_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
repo: ${{ github.repository }}
|
||||
path: release_assets
|
||||
|
||||
- name: Download GUI Artifact
|
||||
@@ -54,7 +50,7 @@ jobs:
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.gui_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
repo: ${{ github.repository }}
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Download Mobile Artifact
|
||||
@@ -62,7 +58,7 @@ jobs:
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.mobile_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
repo: ${{ github.repository }}
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Zip release assets
|
||||
|
||||
@@ -38,6 +38,7 @@ node_modules
|
||||
.vite
|
||||
|
||||
easytier-gui/src-tauri/*.dll
|
||||
easytier-gui/src-tauri/*.sys
|
||||
/easytier-contrib/easytier-ohrs/dist/
|
||||
|
||||
.direnv
|
||||
|
||||
Generated
+215
-204
@@ -8,15 +8,6 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
@@ -312,16 +303,6 @@ dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-event"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1222afd3d2bce3995035054046a279ae7aa154d70d0766cea050073f3fd7ddf"
|
||||
dependencies = [
|
||||
"loom 0.5.6",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.0"
|
||||
@@ -454,9 +435,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.81"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -510,17 +491,6 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "auto-launch"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"thiserror 1.0.63",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto_impl"
|
||||
version = "1.2.1"
|
||||
@@ -663,6 +633,31 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"axum-core 0.5.2",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_path_to_error",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-login"
|
||||
version = "0.16.0"
|
||||
@@ -722,21 +717,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.7.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base62"
|
||||
version = "2.0.2"
|
||||
@@ -1258,9 +1238,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cidr"
|
||||
version = "0.2.3"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
|
||||
checksum = "bd1b64030216239a2e7c364b13cd96a2097ebf0dfe5025f2dedee14a23f2ab60"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1939,16 +1919,6 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diatomic-waker"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28025fb55a9d815acf7b0877555f437254f373036eec6ed265116c7a5c0825e9"
|
||||
dependencies = [
|
||||
"loom 0.5.6",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1999,7 +1969,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.0",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2108,7 +2078,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.4.4"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -2125,6 +2095,7 @@ dependencies = [
|
||||
"bytecodec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"clap",
|
||||
@@ -2137,6 +2108,7 @@ dependencies = [
|
||||
"derive_builder",
|
||||
"easytier-rpc-build 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"encoding",
|
||||
"flume 0.12.0",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"gethostname 0.5.0",
|
||||
@@ -2152,6 +2124,7 @@ dependencies = [
|
||||
"http_req",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"idna 1.0.3",
|
||||
"kcp-sys",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
@@ -2165,11 +2138,13 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"petgraph 0.8.1",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
@@ -2191,12 +2166,12 @@ dependencies = [
|
||||
"serial_test",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
"shellexpand",
|
||||
"smoltcp",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"stun_codec",
|
||||
"sys-locale",
|
||||
"tabled",
|
||||
"tachyonix",
|
||||
"tempfile",
|
||||
"thiserror 1.0.63",
|
||||
"thunk-rs",
|
||||
@@ -2222,6 +2197,7 @@ dependencies = [
|
||||
"which 7.0.3",
|
||||
"wildmatch",
|
||||
"winapi",
|
||||
"windivert",
|
||||
"windows 0.52.0",
|
||||
"windows-service",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -2258,9 +2234,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-gui"
|
||||
version = "2.4.4"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"dunce",
|
||||
@@ -2273,7 +2250,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-positioner",
|
||||
@@ -2313,12 +2289,14 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum 0.8.4",
|
||||
"axum-extra",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"futures",
|
||||
"jsonwebtoken",
|
||||
"mimalloc",
|
||||
"mockall",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
@@ -2345,7 +2323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-web"
|
||||
version = "2.4.4"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2361,6 +2339,7 @@ dependencies = [
|
||||
"image 0.24.9",
|
||||
"imageproc",
|
||||
"maxminddb",
|
||||
"mimalloc",
|
||||
"once_cell",
|
||||
"password-auth",
|
||||
"rand 0.8.5",
|
||||
@@ -2601,6 +2580,15 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etherparse"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "827292ea592108849932ad8e30218f8b1f21c0dfd0696698a18b5d0aed62d990"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
@@ -2639,6 +2627,9 @@ name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
@@ -2699,6 +2690,18 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -2801,9 +2804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -2811,9 +2814,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
@@ -2839,9 +2842,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2858,9 +2861,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2869,15 +2872,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
@@ -2887,9 +2890,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -3011,19 +3014,6 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
@@ -3125,12 +3115,6 @@ dependencies = [
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.4"
|
||||
@@ -3740,7 +3724,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4073,7 +4057,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"widestring",
|
||||
"windows-sys 0.48.0",
|
||||
"winreg 0.50.0",
|
||||
@@ -4265,7 +4249,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kcp-sys"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/EasyTier/kcp-sys?rev=0f0a0558391ba391c089806c23f369651f6c9eeb#0f0a0558391ba391c089806c23f369651f6c9eeb"
|
||||
source = "git+https://github.com/EasyTier/kcp-sys?rev=71eff18c573a4a71bf99c7fabc6a8b9f211c84c1#71eff18c573a4a71bf99c7fabc6a8b9f211c84c1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_impl",
|
||||
@@ -4483,19 +4467,6 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator 0.7.5",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
@@ -4503,7 +4474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator 0.8.4",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -4746,7 +4717,7 @@ dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"loom 0.7.2",
|
||||
"loom",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"rustc_version",
|
||||
@@ -4779,9 +4750,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5449,15 +5420,6 @@ dependencies = [
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -5564,6 +5526,15 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered_hash_map"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6c699f8a30f345785be969deed7eee4c73a5de58c7faf61d6a3251ef798ff61"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.8.2"
|
||||
@@ -6202,6 +6173,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20"
|
||||
dependencies = [
|
||||
"cidr",
|
||||
"ipnet",
|
||||
"num-traits",
|
||||
]
|
||||
@@ -6455,7 +6427,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -6493,7 +6465,7 @@ checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -7065,12 +7037,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
@@ -7527,10 +7493,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.207"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
@@ -7546,10 +7513,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.207"
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7567,6 +7543,19 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_html_form"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap 2.7.1",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.125"
|
||||
@@ -7740,13 +7729,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "service-manager"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b"
|
||||
source = "git+https://github.com/EasyTier/service-manager-rs.git?branch=main#5eb28f7a686858eea4f4933534ed989d3b71dc2a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs 4.0.0",
|
||||
"encoding-utils",
|
||||
"encoding_rs",
|
||||
"plist",
|
||||
"sys-info",
|
||||
"which 4.4.2",
|
||||
"xml-rs",
|
||||
]
|
||||
@@ -7802,6 +7792,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -7917,6 +7916,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "softbuffer"
|
||||
version = "0.4.5"
|
||||
@@ -8193,7 +8202,7 @@ checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
"flume",
|
||||
"flume 0.11.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
@@ -8328,9 +8337,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
@@ -8346,6 +8355,16 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-info"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.1"
|
||||
@@ -8412,20 +8431,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tachyonix"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1924ef47bc3b427ea2a0b55ba97d0e9116e9103483ecd75a43f47a66443527c5"
|
||||
dependencies = [
|
||||
"async-event",
|
||||
"crossbeam-utils",
|
||||
"diatomic-waker",
|
||||
"futures-core",
|
||||
"loom 0.5.6",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
@@ -8625,20 +8630,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "062cdcd483d5e3148c9a64dabf8c574e239e2aa1193cf208d95cf89a676f87a5"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.3.0"
|
||||
@@ -8851,7 +8842,7 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.0.7",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9044,28 +9035,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.1"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"socket2 0.6.1",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -9297,7 +9287,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost",
|
||||
"socket2",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
@@ -9934,12 +9924,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -10370,6 +10354,27 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windivert"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/EasyTier/windivert-rust.git?rev=adcc56d1550f7b5377ec2b3429f413ee24a77375#adcc56d1550f7b5377ec2b3429f413ee24a77375"
|
||||
dependencies = [
|
||||
"etherparse",
|
||||
"thiserror 1.0.63",
|
||||
"windivert-sys",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windivert-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/EasyTier/windivert-rust.git?rev=adcc56d1550f7b5377ec2b3429f413ee24a77375#adcc56d1550f7b5377ec2b3429f413ee24a77375"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"thiserror 1.0.63",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "window-vibrancy"
|
||||
version = "0.6.0"
|
||||
@@ -10423,7 +10428,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -10466,7 +10471,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
@@ -10478,7 +10483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
@@ -10532,6 +10537,12 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
@@ -10539,7 +10550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10568,7 +10579,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10598,7 +10609,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10646,6 +10657,15 @@ dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
@@ -10714,7 +10734,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10933,15 +10953,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
||||
@@ -105,9 +105,9 @@ After successful execution, you can check the network status using `easytier-cli
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.4-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.4-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.4-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.5.0-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.5.0-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.5.0-70e69a38~ |
|
||||
```
|
||||
|
||||
You can test connectivity between nodes:
|
||||
@@ -280,8 +280,6 @@ sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
|
||||
|
||||
### Contact Us
|
||||
|
||||
|
||||
+3
-5
@@ -106,9 +106,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.4-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.4-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.4-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.5.0-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.5.0-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.5.0-70e69a38~ |
|
||||
```
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
@@ -281,8 +281,6 @@ sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/):用于连接设备的全球虚拟网络。
|
||||
- [TailScale](https://tailscale.com/):旨在简化网络配置的 VPN 解决方案。
|
||||
- [vpncloud](https://github.com/dswd/vpncloud):一个 P2P 网状 VPN
|
||||
- [Candy](https://github.com/lanthora/candy):一个可靠、低延迟、反审查的虚拟专用网络
|
||||
|
||||
### 联系我们
|
||||
|
||||
|
||||
@@ -11,6 +11,6 @@ jni = "0.21"
|
||||
once_cell = "1.18.0"
|
||||
log = "0.4"
|
||||
android_logger = "0.13"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0.220", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
easytier = { path = "../../easytier" }
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# EasyTier Android JNI 构建脚本
|
||||
# 用于编译适用于 Android 平台的 JNI 库
|
||||
# 使用 cargo-ndk 工具简化 Android 编译过程
|
||||
|
||||
set -e
|
||||
|
||||
@@ -13,8 +14,8 @@ NC='\033[0m' # No Color
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
echo -e "${GREEN}EasyTier Android JNI 构建脚本${NC}"
|
||||
echo "=============================="
|
||||
echo -e "${GREEN}EasyTier Android JNI 构建脚本 (使用 cargo-ndk)${NC}"
|
||||
echo "=============================================="
|
||||
|
||||
# 检查 Rust 是否安装
|
||||
if ! command -v rustc &> /dev/null; then
|
||||
@@ -28,18 +29,38 @@ if ! command -v cargo &> /dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Android 目标架构
|
||||
# TARGETS=("aarch64-linux-android" "armv7-linux-androideabi" "i686-linux-android" "x86_64-linux-android")
|
||||
TARGETS=("aarch64-linux-android")
|
||||
# 检查 cargo-ndk 是否安装
|
||||
if ! cargo ndk --version &> /dev/null; then
|
||||
echo -e "${YELLOW}cargo-ndk 未安装,正在安装...${NC}"
|
||||
cargo install cargo-ndk
|
||||
if ! cargo ndk --version &> /dev/null; then
|
||||
echo -e "${RED}错误: cargo-ndk 安装失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查是否安装了 Android 目标
|
||||
echo -e "${YELLOW}检查 Android 目标架构...${NC}"
|
||||
for target in "${TARGETS[@]}"; do
|
||||
if ! rustup target list --installed | grep -q "$target"; then
|
||||
echo -e "${YELLOW}安装目标架构: $target${NC}"
|
||||
rustup target add "$target"
|
||||
echo -e "${GREEN}cargo-ndk 版本: $(cargo ndk --version)${NC}"
|
||||
|
||||
# Android 目标架构映射 (cargo-ndk 使用的架构名称)
|
||||
# ANDROID_TARGETS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
|
||||
ANDROID_TARGETS=("arm64-v8a")
|
||||
|
||||
# Android 架构到 Rust target 的映射
|
||||
declare -A TARGET_MAP
|
||||
TARGET_MAP["arm64-v8a"]="aarch64-linux-android"
|
||||
TARGET_MAP["armeabi-v7a"]="armv7-linux-androideabi"
|
||||
TARGET_MAP["x86"]="i686-linux-android"
|
||||
TARGET_MAP["x86_64"]="x86_64-linux-android"
|
||||
|
||||
# 检查并安装所需的 Rust target
|
||||
echo -e "${YELLOW}检查并安装 Android 目标架构...${NC}"
|
||||
for android_target in "${ANDROID_TARGETS[@]}"; do
|
||||
rust_target="${TARGET_MAP[$android_target]}"
|
||||
if ! rustup target list --installed | grep -q "$rust_target"; then
|
||||
echo -e "${YELLOW}安装目标架构: $rust_target (for $android_target)${NC}"
|
||||
rustup target add "$rust_target"
|
||||
else
|
||||
echo -e "${GREEN}目标架构已安装: $target${NC}"
|
||||
echo -e "${GREEN}目标架构已安装: $rust_target (for $android_target)${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -49,66 +70,46 @@ mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# 构建函数
|
||||
build_for_target() {
|
||||
local target=$1
|
||||
echo -e "${YELLOW}构建目标: $target${NC}"
|
||||
|
||||
# 设置环境变量
|
||||
export CC_aarch64_linux_android="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
|
||||
export CC_armv7_linux_androideabi="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
|
||||
export CC_i686_linux_android="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang"
|
||||
export CC_x86_64_linux_android="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang"
|
||||
local android_target=$1
|
||||
echo -e "${YELLOW}构建目标: $android_target${NC}"
|
||||
|
||||
# 首先构建 easytier-ffi
|
||||
echo -e "${YELLOW}构建 easytier-ffi for $target${NC}"
|
||||
(cd $REPO_ROOT/easytier-contrib/easytier-ffi && cargo build --target="$target" --release)
|
||||
|
||||
# 设置链接器环境变量
|
||||
export RUSTFLAGS="-L $(readlink -f $REPO_ROOT/target/$target/release) -l easytier_ffi"
|
||||
echo $RUSTFLAGS
|
||||
echo -e "${YELLOW}构建 easytier-ffi for $android_target${NC}"
|
||||
(cd $REPO_ROOT/easytier-contrib/easytier-ffi && cargo ndk -t $android_target build --release)
|
||||
|
||||
# 构建 JNI 库
|
||||
cargo build --target="$target" --release
|
||||
cargo ndk -t $android_target build --release
|
||||
|
||||
# 复制库文件到输出目录
|
||||
local arch_dir
|
||||
case $target in
|
||||
"aarch64-linux-android")
|
||||
arch_dir="arm64-v8a"
|
||||
;;
|
||||
"armv7-linux-androideabi")
|
||||
arch_dir="armeabi-v7a"
|
||||
;;
|
||||
"i686-linux-android")
|
||||
arch_dir="x86"
|
||||
;;
|
||||
"x86_64-linux-android")
|
||||
arch_dir="x86_64"
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$OUTPUT_DIR/$arch_dir"
|
||||
cp "$REPO_ROOT/target/$target/release/libeasytier_android_jni.so" "$OUTPUT_DIR/$arch_dir/"
|
||||
echo -e "${GREEN}库文件已复制到: $OUTPUT_DIR/$arch_dir/${NC}"
|
||||
# cargo-ndk 使用 Rust target 名称作为目录名,而不是 Android 架构名称
|
||||
rust_target="${TARGET_MAP[$android_target]}"
|
||||
mkdir -p "$OUTPUT_DIR/$android_target"
|
||||
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_android_jni.so" "$OUTPUT_DIR/$android_target/"
|
||||
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_ffi.so" "$OUTPUT_DIR/$android_target/"
|
||||
echo -e "${GREEN}库文件已复制到: $OUTPUT_DIR/$android_target/${NC}"
|
||||
}
|
||||
|
||||
# 检查 Android NDK
|
||||
if [ -z "$ANDROID_NDK_ROOT" ]; then
|
||||
echo -e "${RED}错误: 未设置 ANDROID_NDK_ROOT 环境变量${NC}"
|
||||
echo "请设置 ANDROID_NDK_ROOT 指向您的 Android NDK 安装目录"
|
||||
echo "例如: export ANDROID_NDK_ROOT=/path/to/android-ndk"
|
||||
exit 1
|
||||
# 检查 Android NDK (cargo-ndk 会自动处理 NDK 路径)
|
||||
if [ -z "$ANDROID_NDK_ROOT" ] && [ -z "$ANDROID_NDK_HOME" ] && [ -z "$NDK_HOME" ]; then
|
||||
echo -e "${YELLOW}警告: 未设置 Android NDK 环境变量${NC}"
|
||||
echo "cargo-ndk 将尝试自动检测 NDK 路径"
|
||||
echo "如果构建失败,请设置以下环境变量之一:"
|
||||
echo " - ANDROID_NDK_ROOT"
|
||||
echo " - ANDROID_NDK_HOME"
|
||||
echo " - NDK_HOME"
|
||||
else
|
||||
if [ -n "$ANDROID_NDK_ROOT" ]; then
|
||||
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_ROOT${NC}"
|
||||
elif [ -n "$ANDROID_NDK_HOME" ]; then
|
||||
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_HOME${NC}"
|
||||
elif [ -n "$NDK_HOME" ]; then
|
||||
echo -e "${GREEN}使用 Android NDK: $NDK_HOME${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$ANDROID_NDK_ROOT" ]; then
|
||||
echo -e "${RED}错误: Android NDK 目录不存在: $ANDROID_NDK_ROOT${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_ROOT${NC}"
|
||||
|
||||
# 构建所有目标
|
||||
echo -e "${YELLOW}开始构建所有目标架构...${NC}"
|
||||
for target in "${TARGETS[@]}"; do
|
||||
for target in "${ANDROID_TARGETS[@]}"; do
|
||||
build_for_target "$target"
|
||||
done
|
||||
|
||||
@@ -122,4 +123,7 @@ echo ""
|
||||
echo -e "${YELLOW}使用说明:${NC}"
|
||||
echo "1. 将生成的 .so 文件复制到您的 Android 项目的 src/main/jniLibs/ 目录下"
|
||||
echo "2. 将 java/com/easytier/jni/EasyTierJNI.java 复制到您的 Android 项目中"
|
||||
echo "3. 在您的 Android 代码中调用 EasyTierJNI 类的方法"
|
||||
echo "3. 在您的 Android 代码中调用 EasyTierJNI 类的方法"
|
||||
echo ""
|
||||
echo -e "${GREEN}注意: 此脚本使用 cargo-ndk 工具,无需手动设置复杂的环境变量${NC}"
|
||||
echo -e "${GREEN}cargo-ndk 会自动处理交叉编译所需的工具链配置${NC}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use easytier::proto::web::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
use jni::JNIEnv;
|
||||
|
||||
@@ -2,9 +2,8 @@ use std::sync::Mutex;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::config::{ConfigLoader as _, TomlConfigLoader},
|
||||
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::ConfigSource,
|
||||
};
|
||||
|
||||
static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, uuid::Uuid>> =
|
||||
@@ -129,13 +128,14 @@ pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char)
|
||||
return -1;
|
||||
}
|
||||
|
||||
let instance_id = match INSTANCE_MANAGER.run_network_instance(cfg, ConfigSource::FFI) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let instance_id =
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
|
||||
|
||||
@@ -202,7 +202,7 @@ pub unsafe extern "C" fn collect_network_infos(
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos() {
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
|
||||
@@ -33,5 +33,6 @@ foreign_network_whitelist = "*"
|
||||
disable_p2p = false
|
||||
relay_all_peer_rpc = false
|
||||
disable_udp_hole_punching = false
|
||||
disable_tcp_hole_punching = false
|
||||
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ while true; do
|
||||
|
||||
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
|
||||
if [ -f "${MODDIR}/config/command_args" ]; then
|
||||
TZ=Asia/Shanghai ${EASYTIER} $(cat ${MODDIR}/config/command_args) > ${LOG_FILE} &
|
||||
TZ=Asia/Shanghai ${EASYTIER} $(cat ${MODDIR}/config/command_args) --hostname "$(getprop ro.product.brand)-$(getprop ro.product.model)" > ${LOG_FILE} &
|
||||
sleep 5s # 等待easytier-core启动完成
|
||||
update_module_description "主程序已开启(启动参数模式) | ${REDIR_STATUS}"
|
||||
else
|
||||
TZ=Asia/Shanghai ${EASYTIER} -c ${CONFIG_FILE} > ${LOG_FILE} &
|
||||
TZ=Asia/Shanghai ${EASYTIER} -c ${CONFIG_FILE} --hostname "$(getprop ro.product.brand)-$(getprop ro.product.model)" > ${LOG_FILE} &
|
||||
sleep 5s # 等待easytier-core启动完成
|
||||
update_module_description "主程序已开启(配置文件模式) | ${REDIR_STATUS}"
|
||||
fi
|
||||
|
||||
@@ -22,7 +22,10 @@ get_tun_iface() {
|
||||
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
|
||||
}
|
||||
get_hot_iface() {
|
||||
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|p2p-wlan[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||
}
|
||||
get_usb_iface() {
|
||||
ip link | awk -F': ' '/(^| )(usb[[:alnum:]_]*|rndis[[:alnum:]_]*|eth[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||
}
|
||||
get_hot_cidr() {
|
||||
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
|
||||
@@ -33,10 +36,12 @@ set_nat_rules() {
|
||||
ET_IFACE=$(get_et_iface)
|
||||
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
|
||||
HOT_IFACE=$(get_hot_iface)
|
||||
USB_IFACE=$(get_usb_iface)
|
||||
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
|
||||
USB_CIDR=$(get_hot_cidr "$USB_IFACE")
|
||||
|
||||
# 如果热点关闭就删除自定义链
|
||||
[ -n "$ET_IFACE" ] && [ -n "$HOT_CIDR" ] || return 1
|
||||
[ -n "$ET_IFACE" ] && { [ -n "$HOT_CIDR" ] || [ -n "$USB_CIDR" ]; } || return 1
|
||||
|
||||
# 创建自定义链(如不存在)
|
||||
iptables -t nat -N ET_NAT 2>/dev/null
|
||||
@@ -49,13 +54,22 @@ set_nat_rules() {
|
||||
iptables -I FORWARD 1 -j ET_FWD
|
||||
|
||||
# 添加规则
|
||||
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
|
||||
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
|
||||
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||
if [ -n "$HOT_CIDR" ]; then
|
||||
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
|
||||
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
|
||||
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||
fi
|
||||
if [ -n "$USB_CIDR" ]; then
|
||||
iptables -t nat -A ET_NAT -s "$USB_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||
iptables -A ET_FWD -i "$USB_IFACE" -o "$ET_IFACE" \
|
||||
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A ET_FWD -i "$ET_IFACE" -o "$USB_IFACE" \
|
||||
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
echo "[ET-NAT] Rules applied: $USB_IFACE $USB_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
flush_rules() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.4.4
|
||||
version=v2.5.0
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
dist/
|
||||
target/
|
||||
.DS_Store
|
||||
.idea/
|
||||
package/libs
|
||||
|
||||
*.har
|
||||
|
||||
Cargo.lock
|
||||
+592
-597
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@ crate-type=["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ohos-hilog-binding = {version = "*", features = ["redirect"]}
|
||||
easytier = { git = "https://github.com/EasyTier/EasyTier.git" }
|
||||
napi-derive-ohos = "1.0.4"
|
||||
napi-ohos = { version = "1.0.4", default-features = false, features = [
|
||||
easytier = { path = "../../easytier" }
|
||||
napi-derive-ohos = "1.1"
|
||||
napi-ohos = { version = "1.1", default-features = false, features = [
|
||||
"serde-json",
|
||||
"latin1",
|
||||
"chrono_date",
|
||||
@@ -30,10 +30,15 @@ serde_json = "1.0.125"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-core = "0.1.33"
|
||||
tracing = "0.1.41"
|
||||
uuid = { version = "1.17.0", features = ["v4"] }
|
||||
uuid = { version = "1.5.0", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"serde",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build-ohos = "1.0.4"
|
||||
napi-build-ohos = "1.1"
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
debug = true
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main () {
|
||||
fn main() {
|
||||
napi_build_ohos::setup();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
||||
# 0.0.1
|
||||
- init package
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser 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
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
# `easytier-ohrs`
|
||||
|
||||
## Install
|
||||
|
||||
use `ohpm` to install package.
|
||||
|
||||
```shell
|
||||
ohpm install easytier-ohrs
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### collectNetworkInfos
|
||||
|
||||
```ts
|
||||
collectNetworkInfos(): Array<KeyValuePair>
|
||||
````
|
||||
|
||||
获取正在运行的网络实例的信息。
|
||||
|
||||
---
|
||||
|
||||
### collectRunningNetwork
|
||||
|
||||
```ts
|
||||
collectRunningNetwork(): Array<string>
|
||||
```
|
||||
|
||||
获取当前正在运行的网络实例名称列表。
|
||||
|
||||
---
|
||||
|
||||
### convertTomlToNetworkConfig
|
||||
|
||||
```ts
|
||||
convertTomlToNetworkConfig(cfgStr: string): string
|
||||
```
|
||||
|
||||
将 TOML 配置转换为 NetworkConfig。
|
||||
|
||||
* `cfgStr`:TOML 配置内容
|
||||
|
||||
---
|
||||
|
||||
### defaultNetworkConfig
|
||||
|
||||
```ts
|
||||
defaultNetworkConfig(): string
|
||||
```
|
||||
|
||||
获取默认的网络配置(JSON 字符串),用于转换为object进行赋值。
|
||||
|
||||
---
|
||||
|
||||
### easytierVersion
|
||||
|
||||
```ts
|
||||
easytierVersion(): string
|
||||
```
|
||||
|
||||
获取 EasyTier 当前版本号。
|
||||
|
||||
---
|
||||
|
||||
### hilogGlobalOptions
|
||||
|
||||
```ts
|
||||
hilogGlobalOptions(domain: number, tag: string): void
|
||||
```
|
||||
|
||||
设置全局日志选项。
|
||||
|
||||
* `domain`:日志域 ID
|
||||
* `tag`:日志标签
|
||||
|
||||
---
|
||||
|
||||
### initPanicHook
|
||||
|
||||
```ts
|
||||
initPanicHook(): void
|
||||
```
|
||||
|
||||
初始化 panic 钩子,用于将Rust侧的panic输出到hilog中,请先通过 hilogGlobalOptions 设置hilog的参数。
|
||||
|
||||
---
|
||||
|
||||
### initTracingSubscriber
|
||||
|
||||
```ts
|
||||
initTracingSubscriber(): void
|
||||
```
|
||||
|
||||
初始化 tracing 日志订阅器,用于将Rust侧日志同步输出到hilog中,请先通过 hilogGlobalOptions 设置hilog的参数。
|
||||
|
||||
---
|
||||
|
||||
### isRunningNetwork
|
||||
|
||||
```ts
|
||||
isRunningNetwork(instId: string): boolean
|
||||
```
|
||||
|
||||
判断指定网络实例是否正在运行。
|
||||
|
||||
* `instId`:网络实例 ID
|
||||
|
||||
---
|
||||
|
||||
### parseNetworkConfig
|
||||
|
||||
```ts
|
||||
parseNetworkConfig(cfgJson: string): boolean
|
||||
```
|
||||
|
||||
校验网络配置(JSON 格式)是否合法。
|
||||
|
||||
* `cfgJson`:网络配置内容
|
||||
|
||||
---
|
||||
|
||||
### runNetworkInstance
|
||||
|
||||
```ts
|
||||
runNetworkInstance(cfgJson: string): boolean
|
||||
```
|
||||
|
||||
启动网络实例。
|
||||
|
||||
* `cfgJson`:网络配置(JSON)
|
||||
|
||||
---
|
||||
|
||||
### setTunFd
|
||||
|
||||
```ts
|
||||
setTunFd(instId: string, fd: number): boolean
|
||||
```
|
||||
|
||||
为指定网络实例设置 TUN 设备文件描述符。
|
||||
|
||||
* `instId`:网络实例 ID
|
||||
* `fd`:TUN 设备文件描述符
|
||||
|
||||
---
|
||||
|
||||
### stopNetworkInstance
|
||||
|
||||
```ts
|
||||
stopNetworkInstance(instNames: Array<string>): void
|
||||
```
|
||||
|
||||
停止指定的网络实例。
|
||||
|
||||
* `instNames`:网络实例名称列表
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
// todo
|
||||
```
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import * as api from "libeasytier_ohrs.so";
|
||||
|
||||
export * from 'libeasytier_ohrs.so';
|
||||
export default api;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"license": "LGPL-3.0",
|
||||
"author": "easytier",
|
||||
"name": "easytier-ohrs",
|
||||
"description": "EasyTier for OpenHarmonyOS",
|
||||
"main": "index.ets",
|
||||
"version": "0.0.1",
|
||||
"types": "libs/index.d.ts",
|
||||
"dependencies": {},
|
||||
"compatibleSdkVersion": "17",
|
||||
"compatibleSdkType": "OpenHarmony",
|
||||
"obfuscated": false,
|
||||
"nativeComponents": [
|
||||
{
|
||||
"name": "libeasytier_ohrs.so",
|
||||
"compatibleSdkVersion": "17",
|
||||
"compatibleSdkType": "OpenHarmony"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "easytier-ohrs",
|
||||
"type": "har",
|
||||
"deviceTypes": ["default", "tablet", "2in1"]
|
||||
},
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
mod native_log;
|
||||
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
|
||||
use easytier::common::constants::EASYTIER_VERSION;
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use easytier::launcher::ConfigSource;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use napi_derive_ohos::napi;
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use std::format;
|
||||
@@ -18,23 +19,23 @@ pub struct KeyValuePair {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_tun_fd(
|
||||
inst_id: String,
|
||||
fd: i32,
|
||||
) -> bool {
|
||||
pub fn easytier_version() -> String {
|
||||
EASYTIER_VERSION.to_string()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_tun_fd(inst_id: String, fd: i32) -> bool {
|
||||
match Uuid::try_parse(&inst_id) {
|
||||
Ok(uuid) => {
|
||||
match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) {
|
||||
Ok(_) => {
|
||||
hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e);
|
||||
false
|
||||
}
|
||||
Ok(uuid) => match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) {
|
||||
Ok(_) => {
|
||||
hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id);
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
||||
false
|
||||
@@ -43,11 +44,46 @@ pub fn set_tun_fd(
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_config(cfg_str: String) -> bool {
|
||||
match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(_) => {
|
||||
true
|
||||
pub fn default_network_config() -> String {
|
||||
match NetworkConfig::new_from_config(TomlConfigLoader::default()) {
|
||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] default_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn convert_toml_to_network_config(cfg_str: String) -> String {
|
||||
match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => match NetworkConfig::new_from_config(cfg) {
|
||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_network_config(cfg_json: String) -> bool {
|
||||
match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
||||
Ok(cfg) => match cfg.gen_config() {
|
||||
Ok(toml) => {
|
||||
hilog_debug!("[Rust] Convert to Toml {}", toml.dump());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
false
|
||||
@@ -56,16 +92,22 @@ pub fn parse_config(cfg_str: String) -> bool {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn run_network_instance(cfg_str: String) -> bool {
|
||||
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => cfg,
|
||||
pub fn run_network_instance(cfg_json: String) -> bool {
|
||||
let cfg = match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
||||
Ok(cfg) => match cfg.gen_config() {
|
||||
Ok(toml) => toml,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 {
|
||||
|
||||
if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 {
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
@@ -78,7 +120,7 @@ pub fn run_network_instance(cfg_str: String) -> bool {
|
||||
return false;
|
||||
}
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, ConfigSource::FFI)
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
true
|
||||
}
|
||||
@@ -99,7 +141,7 @@ pub fn stop_network_instance(inst_names: Vec<String>) {
|
||||
#[napi]
|
||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
let mut result = Vec::new();
|
||||
match INSTANCE_MANAGER.collect_network_infos() {
|
||||
match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(map) => {
|
||||
for (uuid, info) in map.iter() {
|
||||
// convert value to json string
|
||||
@@ -134,15 +176,10 @@ pub fn collect_running_network() -> Vec<String> {
|
||||
#[napi]
|
||||
pub fn is_running_network(inst_id: String) -> bool {
|
||||
match Uuid::try_parse(&inst_id) {
|
||||
Ok(uuid) => {
|
||||
INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.contains(&uuid)
|
||||
}
|
||||
Ok(uuid) => INSTANCE_MANAGER.list_network_instance_ids().contains(&uuid),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use napi_derive_ohos::napi;
|
||||
use ohos_hilog_binding::{
|
||||
LogOptions, hilog_debug, hilog_error, hilog_info, hilog_warn, set_global_options,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::panic;
|
||||
use napi_derive_ohos::napi;
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error, hilog_info, hilog_warn, set_global_options, LogOptions};
|
||||
use tracing::{Event, Subscriber};
|
||||
use tracing_core::Level;
|
||||
use tracing_subscriber::layer::{Context, Layer};
|
||||
@@ -20,12 +22,9 @@ pub fn init_panic_hook() {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn hilog_global_options(
|
||||
domain: u32,
|
||||
tag: String,
|
||||
) {
|
||||
pub fn hilog_global_options(domain: u32, tag: String) {
|
||||
ohos_hilog_binding::forward_stdio_to_hilog();
|
||||
set_global_options(LogOptions{
|
||||
set_global_options(LogOptions {
|
||||
domain,
|
||||
tag: Box::leak(tag.clone().into_boxed_str()),
|
||||
})
|
||||
@@ -34,11 +33,9 @@ pub fn hilog_global_options(
|
||||
#[napi]
|
||||
pub fn init_tracing_subscriber() {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
CallbackLayer {
|
||||
callback: Box::new(tracing_callback),
|
||||
}
|
||||
)
|
||||
.with(CallbackLayer {
|
||||
callback: Box::new(tracing_callback),
|
||||
})
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -93,6 +90,7 @@ impl<'a> tracing::field::Visit for FieldCollector<'a> {
|
||||
}
|
||||
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
self.0.insert(field.name().to_string(), format!("{:?}", value));
|
||||
self.0
|
||||
.insert(field.name().to_string(), format!("{:?}", value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Development Environment Configuration
|
||||
SERVER_HOST=127.0.0.1
|
||||
SERVER_PORT=8080
|
||||
DATABASE_PATH=uptime.db
|
||||
DATABASE_MAX_CONNECTIONS=5
|
||||
HEALTH_CHECK_INTERVAL=60
|
||||
HEALTH_CHECK_TIMEOUT=15
|
||||
HEALTH_CHECK_RETRIES=2
|
||||
RUST_LOG=debug
|
||||
LOG_LEVEL=debug
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
|
||||
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
|
||||
CORS_ALLOWED_HEADERS=content-type,authorization
|
||||
NODE_ENV=development
|
||||
API_BASE_URL=/api
|
||||
ENABLE_COMPRESSION=true
|
||||
ENABLE_CORS=true
|
||||
@@ -15,6 +15,7 @@ uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
|
||||
# Axum web framework
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
axum-extra = { version = "0.10", features = ["query"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
|
||||
tower = "0.5"
|
||||
|
||||
@@ -56,6 +57,8 @@ once_cell = "1.19"
|
||||
# EasyTier core
|
||||
easytier = { path = "../../easytier" }
|
||||
|
||||
mimalloc = { version = "*" }
|
||||
|
||||
# Testing
|
||||
[dev-dependencies]
|
||||
mockall = "0.12"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { healthApi } from './api'
|
||||
import {
|
||||
@@ -70,6 +70,20 @@ const menuItems = [
|
||||
}
|
||||
]
|
||||
|
||||
// 根据当前路由计算默认激活的菜单项
|
||||
const activeMenuIndex = computed(() => {
|
||||
const p = route.path
|
||||
if (p.startsWith('/submit')) return 'submit'
|
||||
return 'dashboard'
|
||||
})
|
||||
|
||||
// 处理菜单选择,避免返回 Promise 导致异步补丁问题
|
||||
const handleMenuSelect = (key) => {
|
||||
const item = menuItems.find((i) => i.name === key)
|
||||
if (item && item.path) {
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
checkHealth()
|
||||
// 定期检查健康状态
|
||||
@@ -89,8 +103,8 @@ onMounted(() => {
|
||||
<h1 class="app-title">EasyTier Uptime</h1>
|
||||
</div>
|
||||
|
||||
<el-menu :default-active="route.name" mode="horizontal" class="nav-menu"
|
||||
@select="(key) => router.push(menuItems.find(item => item.name === key)?.path || '/')">
|
||||
<el-menu :default-active="activeMenuIndex" mode="horizontal" class="nav-menu"
|
||||
@select="handleMenuSelect">
|
||||
<el-menu-item v-for="item in menuItems" :key="item.name" :index="item.name">
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
|
||||
@@ -6,6 +6,18 @@ const api = axios.create({
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
// 保证数组参数使用 repeated keys 风格序列化:tags=a&tags=b
|
||||
paramsSerializer: params => {
|
||||
const usp = new URLSearchParams()
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => usp.append(key, v))
|
||||
} else if (value !== undefined && value !== null && value !== '') {
|
||||
usp.append(key, value)
|
||||
}
|
||||
})
|
||||
return usp.toString()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -50,9 +62,15 @@ api.interceptors.response.use(
|
||||
|
||||
// 节点相关API
|
||||
export const nodeApi = {
|
||||
// 获取节点列表
|
||||
async getNodes(params = {}) {
|
||||
const response = await api.get('/api/nodes', { params })
|
||||
// 获取节点列表(支持传入 AbortController.signal 用于取消)
|
||||
async getNodes(params = {}, options = {}) {
|
||||
const response = await api.get('/api/nodes', { params, signal: options.signal })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取所有标签
|
||||
async getAllTags() {
|
||||
const response = await api.get('/api/tags')
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -149,6 +167,28 @@ export const adminApi = {
|
||||
async updateNode(id, data) {
|
||||
const response = await api.put(`/api/admin/nodes/${id}`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 兼容方法:获取所有节点(参数转换)
|
||||
async getAllNodes(params = {}) {
|
||||
const mapped = {
|
||||
page: params.page,
|
||||
per_page: params.page_size ?? params.per_page,
|
||||
is_approved: params.approved ?? params.is_approved,
|
||||
is_active: params.online ?? params.is_active,
|
||||
protocol: params.protocol,
|
||||
search: params.search,
|
||||
tag: params.tag
|
||||
}
|
||||
// 移除未定义的字段
|
||||
Object.keys(mapped).forEach(k => {
|
||||
if (mapped[k] === undefined || mapped[k] === null || mapped[k] === '') {
|
||||
delete mapped[k]
|
||||
}
|
||||
})
|
||||
// 直接复用现有接口
|
||||
const response = await api.get('/api/admin/nodes', { params: mapped })
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,15 @@
|
||||
<div class="form-tip">详细描述有助于用户选择合适的节点</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新增:标签管理(仅在管理员编辑时显示) -->
|
||||
<el-form-item v-if="props.showTags" label="标签" prop="tags">
|
||||
<el-select v-model="form.tags" multiple filterable allow-create default-first-option :multiple-limit="10"
|
||||
placeholder="输入后按回车添加,如:北京、联通、IPv6、高带宽">
|
||||
<el-option v-for="opt in (form.tags || [])" :key="opt" :label="opt" :value="opt" />
|
||||
</el-select>
|
||||
<div class="form-tip">用于分类与检索,建议 1-6 个标签,每个不超过 32 字符</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<el-form-item label="联系方式" prop="contact_info">
|
||||
<div class="contact-section">
|
||||
@@ -238,6 +247,7 @@ const props = defineProps({
|
||||
wechat: '',
|
||||
qq_number: '',
|
||||
mail: '',
|
||||
tags: [],
|
||||
agreed: false
|
||||
})
|
||||
},
|
||||
@@ -264,6 +274,11 @@ const props = defineProps({
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 新增:是否显示标签管理
|
||||
showTags: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -353,6 +368,38 @@ const rules = {
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
// 新增:标签规则(仅在显示标签管理时生效)
|
||||
tags: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!props.showTags) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(form.tags)) {
|
||||
callback(new Error('标签格式错误'))
|
||||
return
|
||||
}
|
||||
if (form.tags.length > 10) {
|
||||
callback(new Error('最多添加 10 个标签'))
|
||||
return
|
||||
}
|
||||
for (const t of form.tags) {
|
||||
const s = (t || '').trim()
|
||||
if (s.length === 0) {
|
||||
callback(new Error('标签不能为空'))
|
||||
return
|
||||
}
|
||||
if (s.length > 32) {
|
||||
callback(new Error('每个标签不超过 32 字符'))
|
||||
return
|
||||
}
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -362,7 +409,7 @@ const canTest = computed(() => {
|
||||
})
|
||||
|
||||
const buildDataFromForm = () => {
|
||||
return {
|
||||
const data = {
|
||||
name: form.name || 'Test Node',
|
||||
host: form.host,
|
||||
port: form.port,
|
||||
@@ -376,6 +423,11 @@ const buildDataFromForm = () => {
|
||||
qq_number: form.qq_number || null,
|
||||
mail: form.mail || null
|
||||
}
|
||||
// 仅在管理员编辑时附带标签
|
||||
if (props.showTags) {
|
||||
data.tags = Array.isArray(form.tags) ? form.tags : []
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
@@ -441,6 +493,10 @@ const resetFields = () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
// 重置标签
|
||||
if (props.showTags) {
|
||||
form.tags = []
|
||||
}
|
||||
testResult.value = null
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Deterministic tag color generator (pure frontend)
|
||||
// Same tag => same color; different tags => different colors
|
||||
|
||||
function stringHash(str) {
|
||||
const s = String(str)
|
||||
let hash = 5381
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
hash = (hash * 33) ^ s.charCodeAt(i)
|
||||
}
|
||||
return hash >>> 0 // ensure positive
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
// h,s,l in [0,1]
|
||||
let r, g, b
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l // achromatic
|
||||
} else {
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t
|
||||
if (t < 1 / 2) return q
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
|
||||
return p
|
||||
}
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
const p = 2 * l - q
|
||||
r = hue2rgb(p, q, h + 1 / 3)
|
||||
g = hue2rgb(p, q, h)
|
||||
b = hue2rgb(p, q, h - 1 / 3)
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
const toHex = (v) => v.toString(16).padStart(2, '0')
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
export function getTagStyle(tag) {
|
||||
const hash = stringHash(tag)
|
||||
const hue = hash % 360 // 0-359
|
||||
const saturation = 65 // percentage
|
||||
const lightness = 47 // percentage
|
||||
|
||||
const rgb = hslToRgb(hue / 360, saturation / 100, lightness / 100)
|
||||
const hex = rgbToHex(rgb[0], rgb[1], rgb[2])
|
||||
|
||||
// Perceived brightness for text color selection
|
||||
const brightness = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114
|
||||
const textColor = brightness > 160 ? '#1f1f1f' : '#ffffff'
|
||||
|
||||
return {
|
||||
backgroundColor: hex,
|
||||
borderColor: hex,
|
||||
color: textColor
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,17 @@
|
||||
|
||||
<el-table-column prop="description" label="描述" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="tags" label="标签" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="tags-list">
|
||||
<el-tag v-for="(tag, idx) in row.tags" :key="tag + idx" size="small" class="tag-chip" :style="getTagStyle(tag)">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<span v-if="!row.tags || row.tags.length === 0" class="text-muted">无</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
@@ -228,8 +239,8 @@
|
||||
<!-- 编辑节点对话框 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑节点" width="800px" destroy-on-close>
|
||||
<NodeForm v-if="editDialogVisible" v-model="editForm" :submitting="updating" submit-text="更新节点" submit-icon="Edit"
|
||||
:show-connection-test="false" :show-agreement="false" :show-cancel="true" @submit="handleUpdateNode"
|
||||
@cancel="editDialogVisible = false" @reset="resetEditForm" />
|
||||
:show-connection-test="false" :show-agreement="false" :show-cancel="true" :show-tags="true"
|
||||
@submit="handleUpdateNode" @cancel="editDialogVisible = false" @reset="resetEditForm" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -240,6 +251,7 @@ import dayjs from 'dayjs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Check, Clock, DataAnalysis, CircleCheck, Loading } from '@element-plus/icons-vue'
|
||||
import NodeForm from '../components/NodeForm.vue'
|
||||
import { getTagStyle } from '../utils/tagColor'
|
||||
|
||||
export default {
|
||||
name: 'AdminDashboard',
|
||||
@@ -270,7 +282,8 @@ export default {
|
||||
protocol: 'tcp',
|
||||
version: '',
|
||||
max_connections: 100,
|
||||
description: ''
|
||||
description: '',
|
||||
tags: []
|
||||
},
|
||||
editingNodeId: null,
|
||||
updating: false
|
||||
@@ -302,6 +315,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTagStyle,
|
||||
async loadNodes() {
|
||||
try {
|
||||
this.loading = true
|
||||
@@ -379,13 +393,47 @@ export default {
|
||||
},
|
||||
editNode(node) {
|
||||
this.editingNodeId = node.id
|
||||
this.editForm = node
|
||||
// 只取需要的字段,并复制 tags 数组以避免引用问题
|
||||
this.editForm = {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
host: node.host,
|
||||
port: node.port,
|
||||
protocol: node.protocol,
|
||||
version: node.version,
|
||||
max_connections: node.max_connections,
|
||||
description: node.description || '',
|
||||
allow_relay: node.allow_relay,
|
||||
network_name: node.network_name,
|
||||
network_secret: node.network_secret,
|
||||
wechat: node.wechat,
|
||||
qq_number: node.qq_number,
|
||||
mail: node.mail,
|
||||
tags: Array.isArray(node.tags) ? [...node.tags] : []
|
||||
}
|
||||
this.editDialogVisible = true
|
||||
},
|
||||
async handleUpdateNode(formData) {
|
||||
try {
|
||||
this.updating = true
|
||||
await adminApi.updateNode(this.editingNodeId, formData)
|
||||
// 确保提交包含 tags 字段(为空数组也传)
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
host: formData.host,
|
||||
port: formData.port,
|
||||
protocol: formData.protocol,
|
||||
version: formData.version,
|
||||
max_connections: formData.max_connections,
|
||||
description: formData.description,
|
||||
allow_relay: formData.allow_relay,
|
||||
network_name: formData.network_name,
|
||||
network_secret: formData.network_secret,
|
||||
wechat: formData.wechat,
|
||||
qq_number: formData.qq_number,
|
||||
mail: formData.mail,
|
||||
tags: Array.isArray(formData.tags) ? formData.tags : []
|
||||
}
|
||||
await adminApi.updateNode(this.editingNodeId, payload)
|
||||
ElMessage.success('节点更新成功')
|
||||
this.editDialogVisible = false
|
||||
await this.loadNodes()
|
||||
@@ -576,4 +624,8 @@ export default {
|
||||
.text-secondary {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<el-card class="filter-card">
|
||||
<el-row :gutter="20">
|
||||
<el-row :gutter="26">
|
||||
<el-col :span="8">
|
||||
<el-input v-model="searchText" placeholder="搜索节点名称、主机地址或描述" prefix-icon="Search" clearable
|
||||
@input="handleSearch" />
|
||||
@@ -77,14 +77,16 @@
|
||||
<el-option label="WSS" value="wss" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<!-- 新增:标签多选筛选 -->
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" @click="refreshData" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-select v-model="selectedTags" multiple collapse-tags collapse-tags-tooltip filterable clearable
|
||||
placeholder="按标签筛选(可多选)" @change="handleFilter">
|
||||
<el-option v-for="tag in allTags" :key="tag" :label="tag" :value="tag">
|
||||
<span class="tag-option" :style="getTagStyle(tag)">{{ tag }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="4">
|
||||
<el-button type="success" @click="$router.push('/submit')">
|
||||
<el-icon>
|
||||
@@ -97,17 +99,24 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 节点列表 -->
|
||||
<el-card class="nodes-card">
|
||||
<el-card ref="nodesCardRef" class="nodes-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>节点列表</span>
|
||||
<span>
|
||||
节点列表
|
||||
<el-button type="text" :loading="loading" @click="refreshData" style="margin-left: 8px;">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
<el-tag :type="loading ? 'info' : 'success'">
|
||||
{{ loading ? '加载中...' : `共 ${pagination.total} 个节点` }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="nodes" v-loading="loading" stripe style="width: 100%" row-key="id">
|
||||
<el-table ref="tableRef" :data="nodes" v-loading="loading" stripe style="width: 100%" row-key="id">
|
||||
<!-- 展开列 -->
|
||||
<el-table-column type="expand" width="50">
|
||||
<template #default="{ row }">
|
||||
@@ -151,7 +160,7 @@
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; flex-direction: column; gap: 1px; align-items: flex-start;">
|
||||
<el-tag v-if="row.version" size="small" style="font-size: 11px; padding: 1px 4px;">{{ row.version
|
||||
}}</el-tag>
|
||||
}}</el-tag>
|
||||
<span v-else class="text-muted" style="font-size: 11px;">未知</span>
|
||||
<el-tag :type="row.allow_relay ? 'success' : 'info'" size="small"
|
||||
style="font-size: 9px; padding: 1px 3px;">
|
||||
@@ -176,6 +185,18 @@
|
||||
<span class="description">{{ row.description || '暂无描述' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 新增:标签展示 -->
|
||||
<el-table-column label="标签" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="tags-list">
|
||||
<el-tag v-for="(tag, idx) in row.tags" :key="tag + idx" size="small" class="tag-chip"
|
||||
:style="getTagStyle(tag)" style="margin: 2px 6px 2px 0;">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<span v-if="!row.tags || row.tags.length === 0" class="text-muted">无</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
@@ -223,6 +244,16 @@
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(selectedNode.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(selectedNode.updated_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ selectedNode.description || '暂无描述' }}</el-descriptions-item>
|
||||
<!-- 新增:标签 -->
|
||||
<el-descriptions-item label="标签" :span="2">
|
||||
<div class="tags-list">
|
||||
<el-tag v-for="(tag, idx) in selectedNode.tags" :key="tag + idx" size="small" class="tag-chip"
|
||||
style="margin: 2px 6px 2px 0;">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<span v-if="!selectedNode.tags || selectedNode.tags.length === 0" class="text-muted">无</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 健康状态统计 -->
|
||||
@@ -261,7 +292,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nodeApi } from '../api'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -276,6 +307,7 @@ import {
|
||||
Refresh,
|
||||
Plus
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getTagStyle } from '../utils/tagColor'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -283,11 +315,18 @@ const nodes = ref([])
|
||||
const searchText = ref('')
|
||||
const statusFilter = ref('')
|
||||
const protocolFilter = ref('')
|
||||
const selectedTags = ref([])
|
||||
const allTags = ref([])
|
||||
const detailDialogVisible = ref(false)
|
||||
const selectedNode = ref(null)
|
||||
const healthStats = ref(null)
|
||||
const expandedRows = ref([])
|
||||
const apiUrl = ref(window.location.href)
|
||||
const tableRef = ref(null)
|
||||
const nodesCardRef = ref(null)
|
||||
|
||||
// 请求取消控制(避免重复请求覆盖)
|
||||
let fetchController = null
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
@@ -309,6 +348,17 @@ const averageUptime = computed(() => {
|
||||
})
|
||||
|
||||
// 方法
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const resp = await nodeApi.getAllTags()
|
||||
if (resp.success && Array.isArray(resp.data)) {
|
||||
allTags.value = resp.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNodes = async (with_loading = true) => {
|
||||
try {
|
||||
if (with_loading) {
|
||||
@@ -328,13 +378,26 @@ const fetchNodes = async (with_loading = true) => {
|
||||
if (protocolFilter.value) {
|
||||
params.protocol = protocolFilter.value
|
||||
}
|
||||
if (selectedTags.value && selectedTags.value.length > 0) {
|
||||
params.tags = selectedTags.value
|
||||
}
|
||||
|
||||
const response = await nodeApi.getNodes(params)
|
||||
// 取消上一请求,创建新的请求控制器
|
||||
if (fetchController) {
|
||||
try { fetchController.abort() } catch (_) { }
|
||||
}
|
||||
fetchController = new AbortController()
|
||||
|
||||
const response = await nodeApi.getNodes(params, { signal: fetchController.signal })
|
||||
if (response.success && response.data) {
|
||||
nodes.value = response.data.items
|
||||
pagination.total = response.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'CanceledError' || error.name === 'AbortError') {
|
||||
// 被取消的旧请求,忽略
|
||||
return
|
||||
}
|
||||
console.error('获取节点列表失败:', error)
|
||||
ElMessage.error('获取节点列表失败')
|
||||
} finally {
|
||||
@@ -345,6 +408,7 @@ const fetchNodes = async (with_loading = true) => {
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
pagination.page = 1
|
||||
fetchNodes()
|
||||
}
|
||||
|
||||
@@ -408,12 +472,69 @@ const copyAddress = (address) => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchTags()
|
||||
fetchNodes()
|
||||
|
||||
// 设置定时刷新
|
||||
setInterval(() => {
|
||||
fetchNodes(false)
|
||||
}, 3000) // 每30秒刷新一次
|
||||
}, 30000) // 每30秒刷新一次
|
||||
})
|
||||
|
||||
// 智能滚动处理:纵向滚动时页面整体滚动,横向滚动时表格内部滚动
|
||||
let wheelHandler = null
|
||||
let wheelTargets = []
|
||||
|
||||
const detachWheelHandlers = () => {
|
||||
if (wheelTargets && wheelTargets.length) {
|
||||
wheelTargets.forEach((el) => {
|
||||
try { el.removeEventListener('wheel', wheelHandler, { capture: true }) } catch (_) { }
|
||||
})
|
||||
}
|
||||
wheelTargets = []
|
||||
}
|
||||
|
||||
const attachWheelHandler = () => {
|
||||
const tableEl = tableRef.value?.$el
|
||||
const body = tableEl ? tableEl.querySelector('.el-table__body-wrapper') : null
|
||||
if (!body) return
|
||||
|
||||
detachWheelHandlers()
|
||||
const wrap = body.querySelector('.el-scrollbar__wrap') || body
|
||||
|
||||
wheelHandler = (e) => {
|
||||
const deltaX = e.deltaX
|
||||
const deltaY = e.deltaY
|
||||
|
||||
// 如果是横向滚动(Shift + 滚轮 或 触摸板横向滑动)
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) || e.shiftKey) {
|
||||
// 允许表格内部横向滚动,不阻止默认行为
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是纵向滚动,阻止表格内部滚动,让页面整体滚动
|
||||
if (deltaY) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const scroller = document.scrollingElement || document.documentElement
|
||||
scroller.scrollTop += deltaY
|
||||
}
|
||||
}
|
||||
|
||||
body.addEventListener('wheel', wheelHandler, { passive: false, capture: true })
|
||||
wheelTargets.push(body)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(attachWheelHandler)
|
||||
})
|
||||
|
||||
watch(nodes, () => {
|
||||
nextTick(attachWheelHandler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
detachWheelHandlers()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -570,4 +691,28 @@ onMounted(() => {
|
||||
background-color: #fafafa;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.tag-option {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper .el-scrollbar__wrap) {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,11 +18,11 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:11030',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/health': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:11030',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::{Div, Mul};
|
||||
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
|
||||
@@ -16,6 +16,7 @@ use crate::api::{
|
||||
use crate::db::entity::{self, health_records, shared_nodes};
|
||||
use crate::db::{operations::*, Db};
|
||||
use crate::health_checker_manager::HealthCheckerManager;
|
||||
use axum_extra::extract::Query;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -60,6 +61,35 @@ pub async fn get_nodes(
|
||||
);
|
||||
}
|
||||
|
||||
// 标签过滤(支持单标签与多标签 OR)
|
||||
let mut filtered_ids: Option<Vec<i32>> = None;
|
||||
if !filters.tags.is_empty() {
|
||||
let ids_any =
|
||||
NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &filters.tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
// 合并去重
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
if let Some(ids) = filtered_ids {
|
||||
if ids.is_empty() {
|
||||
return Ok(Json(ApiResponse::success(PaginatedResponse {
|
||||
items: vec![],
|
||||
total: 0,
|
||||
page,
|
||||
per_page,
|
||||
total_pages: 0,
|
||||
})));
|
||||
}
|
||||
query = query.filter(entity::shared_nodes::Column::Id.is_in(ids));
|
||||
}
|
||||
|
||||
let total = query.clone().count(app_state.db.orm_db()).await?;
|
||||
let nodes = query
|
||||
.order_by_asc(entity::shared_nodes::Column::Id)
|
||||
@@ -71,6 +101,13 @@ pub async fn get_nodes(
|
||||
let mut node_responses: Vec<NodeResponse> = nodes.into_iter().map(NodeResponse::from).collect();
|
||||
let total_pages = total.div_ceil(per_page as u64);
|
||||
|
||||
// 补充标签
|
||||
let ids: Vec<i32> = node_responses.iter().map(|n| n.id).collect();
|
||||
let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?;
|
||||
for n in &mut node_responses {
|
||||
n.tags = tags_map.get(&n.id).cloned().unwrap_or_default();
|
||||
}
|
||||
|
||||
// 为每个节点添加健康状态信息
|
||||
for node_response in &mut node_responses {
|
||||
if let Some(mut health_record) = app_state
|
||||
@@ -99,7 +136,6 @@ pub async fn get_nodes(
|
||||
|
||||
// remove sensitive information
|
||||
node_responses.iter_mut().for_each(|node| {
|
||||
tracing::info!("node: {:?}", node);
|
||||
node.network_name = None;
|
||||
node.network_secret = None;
|
||||
|
||||
@@ -161,7 +197,10 @@ pub async fn get_node(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(node))))
|
||||
let mut resp = NodeResponse::from(node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn get_node_health(
|
||||
@@ -325,6 +364,39 @@ pub async fn admin_get_nodes(
|
||||
);
|
||||
}
|
||||
|
||||
// 标签过滤(支持单标签与多标签 OR)
|
||||
let mut filtered_ids: Option<Vec<i32>> = None;
|
||||
if let Some(tag) = filters.tag {
|
||||
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
|
||||
filtered_ids = Some(ids);
|
||||
}
|
||||
if let Some(tags) = filters.tags {
|
||||
if !tags.is_empty() {
|
||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(ids) = filtered_ids {
|
||||
if ids.is_empty() {
|
||||
return Ok(Json(ApiResponse::success(PaginatedResponse {
|
||||
items: vec![],
|
||||
total: 0,
|
||||
page,
|
||||
per_page,
|
||||
total_pages: 0,
|
||||
})));
|
||||
}
|
||||
query = query.filter(entity::shared_nodes::Column::Id.is_in(ids));
|
||||
}
|
||||
|
||||
let total = query.clone().count(app_state.db.orm_db()).await?;
|
||||
|
||||
let nodes = query
|
||||
@@ -334,7 +406,14 @@ pub async fn admin_get_nodes(
|
||||
.all(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
let node_responses: Vec<NodeResponse> = nodes.into_iter().map(NodeResponse::from).collect();
|
||||
let mut node_responses: Vec<NodeResponse> = nodes.into_iter().map(NodeResponse::from).collect();
|
||||
|
||||
// 补充标签
|
||||
let ids: Vec<i32> = node_responses.iter().map(|n| n.id).collect();
|
||||
let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?;
|
||||
for n in &mut node_responses {
|
||||
n.tags = tags_map.get(&n.id).cloned().unwrap_or_default();
|
||||
}
|
||||
|
||||
let total_pages = (total as f64 / per_page as f64).ceil() as u32;
|
||||
|
||||
@@ -366,7 +445,10 @@ pub async fn admin_approve_node(
|
||||
.exec(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(updated_node))))
|
||||
let mut resp = NodeResponse::from(updated_node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn admin_update_node(
|
||||
@@ -432,7 +514,15 @@ pub async fn admin_update_node(
|
||||
.exec(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(updated_node))))
|
||||
// 更新标签
|
||||
if let Some(tags) = request.tags {
|
||||
NodeOperations::set_node_tags(&app_state.db, updated_node.id, tags).await?;
|
||||
}
|
||||
|
||||
let mut resp = NodeResponse::from(updated_node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn admin_revoke_approval(
|
||||
@@ -454,7 +544,10 @@ pub async fn admin_revoke_approval(
|
||||
.exec(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(updated_node))))
|
||||
let mut resp = NodeResponse::from(updated_node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn admin_delete_node(
|
||||
@@ -505,3 +598,10 @@ fn verify_admin_token(headers: &HeaderMap) -> ApiResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_all_tags(
|
||||
State(app_state): State<AppState>,
|
||||
) -> ApiResult<Json<ApiResponse<Vec<String>>>> {
|
||||
let tags = NodeOperations::get_all_tags(&app_state.db).await?;
|
||||
Ok(Json(ApiResponse::success(tags)))
|
||||
}
|
||||
|
||||
@@ -162,6 +162,9 @@ pub struct UpdateNodeRequest {
|
||||
|
||||
#[validate(email)]
|
||||
pub mail: Option<String>,
|
||||
|
||||
// 标签字段(仅管理员可用)
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -198,6 +201,7 @@ pub struct NodeResponse {
|
||||
pub qq_number: Option<String>,
|
||||
pub wechat: Option<String>,
|
||||
pub mail: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<entity::shared_nodes::Model> for NodeResponse {
|
||||
@@ -247,6 +251,7 @@ impl From<entity::shared_nodes::Model> for NodeResponse {
|
||||
} else {
|
||||
Some(node.mail)
|
||||
},
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,6 +286,8 @@ pub struct NodeFilterParams {
|
||||
pub is_active: Option<bool>,
|
||||
pub protocol: Option<String>,
|
||||
pub search: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -313,4 +320,6 @@ pub struct AdminNodeFilterParams {
|
||||
pub is_approved: Option<bool>,
|
||||
pub protocol: Option<String>,
|
||||
pub search: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use tower_http::cors::CorsLayer;
|
||||
use super::handlers::AppState;
|
||||
use super::handlers::{
|
||||
admin_approve_node, admin_delete_node, admin_get_nodes, admin_login, admin_revoke_approval,
|
||||
admin_update_node, admin_verify_token, create_node, get_node, get_node_health,
|
||||
admin_update_node, admin_verify_token, create_node, get_all_tags, get_node, get_node_health,
|
||||
get_node_health_stats, get_nodes, health_check,
|
||||
};
|
||||
use crate::api::{get_node_connect_url, test_connection};
|
||||
@@ -38,6 +38,7 @@ pub fn create_routes() -> Router<AppState> {
|
||||
.route("/node/{id}", get(get_node_connect_url))
|
||||
.route("/health", get(health_check))
|
||||
.route("/api/nodes", get(get_nodes).post(create_node))
|
||||
.route("/api/tags", get(get_all_tags))
|
||||
.route("/api/test_connection", post(test_connection))
|
||||
.route("/api/nodes/{id}/health", get(get_node_health))
|
||||
.route("/api/nodes/{id}/health/stats", get(get_node_health_stats))
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::env;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use easytier::common::config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfig};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub server: ServerConfig,
|
||||
@@ -32,12 +34,6 @@ pub struct HealthCheckConfig {
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
pub rust_log: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CorsConfig {
|
||||
pub allowed_origins: Vec<String>,
|
||||
@@ -100,8 +96,14 @@ impl AppConfig {
|
||||
};
|
||||
|
||||
let logging_config = LoggingConfig {
|
||||
level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
|
||||
rust_log: env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||
file_logger: Some(FileLoggerConfig {
|
||||
level: Some(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string())),
|
||||
file: Some("easytier-uptime.log".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
console_logger: Some(ConsoleLoggerConfig {
|
||||
level: Some(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string())),
|
||||
}),
|
||||
};
|
||||
|
||||
let cors_config = CorsConfig {
|
||||
@@ -161,8 +163,14 @@ impl AppConfig {
|
||||
max_retries: 3,
|
||||
},
|
||||
logging: LoggingConfig {
|
||||
level: "info".to_string(),
|
||||
rust_log: "info".to_string(),
|
||||
file_logger: Some(FileLoggerConfig {
|
||||
level: Some("info".to_string()),
|
||||
file: Some("easytier-uptime.log".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
console_logger: Some(ConsoleLoggerConfig {
|
||||
level: Some("info".to_string()),
|
||||
}),
|
||||
},
|
||||
cors: CorsConfig {
|
||||
allowed_origins: vec![
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
pub mod prelude;
|
||||
|
||||
pub mod health_records;
|
||||
pub mod node_tags;
|
||||
pub mod shared_nodes;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! `SeaORM` Entity for node tags
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "node_tags")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub node_id: i32,
|
||||
pub tag: String,
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::shared_nodes::Entity",
|
||||
from = "Column::NodeId",
|
||||
to = "super::shared_nodes::Column::Id"
|
||||
)]
|
||||
SharedNodes,
|
||||
}
|
||||
|
||||
impl Related<super::shared_nodes::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SharedNodes.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
pub use super::health_records::Entity as HealthRecords;
|
||||
pub use super::node_tags::Entity as NodeTags;
|
||||
pub use super::shared_nodes::Entity as SharedNodes;
|
||||
|
||||
@@ -33,6 +33,9 @@ pub struct Model {
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::health_records::Entity")]
|
||||
HealthRecords,
|
||||
// add relation to node_tags
|
||||
#[sea_orm(has_many = "super::node_tags::Entity")]
|
||||
NodeTags,
|
||||
}
|
||||
|
||||
impl Related<super::health_records::Entity> for Entity {
|
||||
@@ -41,4 +44,10 @@ impl Related<super::health_records::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::node_tags::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::NodeTags.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::db::Db;
|
||||
use crate::db::HealthStats;
|
||||
use crate::db::HealthStatus;
|
||||
use sea_orm::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// 节点管理操作
|
||||
pub struct NodeOperations;
|
||||
@@ -229,6 +230,128 @@ impl HealthOperations {
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
}
|
||||
impl NodeOperations {
|
||||
/// 获取节点的全部标签
|
||||
pub async fn get_node_tags(db: &Db, node_id: i32) -> Result<Vec<String>, DbErr> {
|
||||
let tags = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::NodeId.eq(node_id))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
Ok(tags.into_iter().map(|m| m.tag).collect())
|
||||
}
|
||||
|
||||
/// 批量获取节点的标签映射
|
||||
pub async fn get_nodes_tags_map(
|
||||
db: &Db,
|
||||
node_ids: &[i32],
|
||||
) -> Result<HashMap<i32, Vec<String>>, DbErr> {
|
||||
if node_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let tags = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::NodeId.is_in(node_ids.to_vec()))
|
||||
.order_by_asc(node_tags::Column::NodeId)
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
let mut map: HashMap<i32, Vec<String>> = HashMap::new();
|
||||
for t in tags {
|
||||
map.entry(t.node_id).or_default().push(t.tag);
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 使用标签过滤节点(返回节点ID)
|
||||
pub async fn filter_node_ids_by_tag(db: &Db, tag: &str) -> Result<Vec<i32>, DbErr> {
|
||||
let tagged = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::Tag.eq(tag))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
Ok(tagged.into_iter().map(|m| m.node_id).collect())
|
||||
}
|
||||
|
||||
/// 设置节点标签(替换为给定集合)
|
||||
pub async fn set_node_tags(db: &Db, node_id: i32, tags: Vec<String>) -> Result<(), DbErr> {
|
||||
// 去重与清理空白
|
||||
let mut set: HashSet<String> = HashSet::new();
|
||||
for tag in tags.into_iter() {
|
||||
let trimmed = tag.trim();
|
||||
if !trimmed.is_empty() {
|
||||
set.insert(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 取出当前标签
|
||||
let existing = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::NodeId.eq(node_id))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
|
||||
let existing_set: HashSet<String> = existing.iter().map(|m| m.tag.clone()).collect();
|
||||
|
||||
// 需要删除的
|
||||
let to_delete: Vec<i32> = existing
|
||||
.iter()
|
||||
.filter(|m| !set.contains(&m.tag))
|
||||
.map(|m| m.id)
|
||||
.collect();
|
||||
|
||||
// 需要新增的
|
||||
let to_insert: Vec<String> = set
|
||||
.into_iter()
|
||||
.filter(|t| !existing_set.contains(t))
|
||||
.collect();
|
||||
|
||||
// 执行删除
|
||||
if !to_delete.is_empty() {
|
||||
node_tags::Entity::delete_many()
|
||||
.filter(node_tags::Column::Id.is_in(to_delete))
|
||||
.exec(db.orm_db())
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 执行新增
|
||||
for t in to_insert {
|
||||
let now = chrono::Utc::now().fixed_offset();
|
||||
let am = node_tags::ActiveModel {
|
||||
id: NotSet,
|
||||
node_id: Set(node_id),
|
||||
tag: Set(t),
|
||||
created_at: Set(now),
|
||||
};
|
||||
node_tags::Entity::insert(am).exec(db.orm_db()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 新增:获取所有唯一标签(按字母排序)
|
||||
pub async fn get_all_tags(db: &Db) -> Result<Vec<String>, DbErr> {
|
||||
let rows = node_tags::Entity::find().all(db.orm_db()).await?;
|
||||
let mut set: HashSet<String> = HashSet::new();
|
||||
for r in rows {
|
||||
set.insert(r.tag);
|
||||
}
|
||||
let mut list: Vec<String> = set.into_iter().collect();
|
||||
list.sort();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
// 新增:使用多标签(OR 语义)过滤节点,返回匹配的节点ID
|
||||
pub async fn filter_node_ids_by_tags_any(db: &Db, tags: &[String]) -> Result<Vec<i32>, DbErr> {
|
||||
if tags.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let tagged = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::Tag.is_in(tags.to_vec()))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
let mut set: HashSet<i32> = HashSet::new();
|
||||
for m in tagged {
|
||||
set.insert(m.node_id);
|
||||
}
|
||||
Ok(set.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -8,12 +8,11 @@ use anyhow::Context as _;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
|
||||
config::{ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
|
||||
scoped_task::ScopedTask,
|
||||
},
|
||||
defer,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::ConfigSource,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::any;
|
||||
@@ -375,6 +374,7 @@ impl HealthChecker {
|
||||
flags.no_tun = true;
|
||||
flags.disable_p2p = true;
|
||||
flags.disable_udp_hole_punching = true;
|
||||
flags.disable_tcp_hole_punching = true;
|
||||
cfg.set_flags(flags);
|
||||
|
||||
Ok(cfg)
|
||||
@@ -392,7 +392,7 @@ impl HealthChecker {
|
||||
.delete_network_instance(vec![cfg.get_id()]);
|
||||
});
|
||||
self.instance_mgr
|
||||
.run_network_instance(cfg.clone(), ConfigSource::FFI)
|
||||
.run_network_instance(cfg.clone(), false, ConfigFileControl::STATIC_CONFIG)
|
||||
.with_context(|| "failed to run network instance")?;
|
||||
|
||||
let now = Instant::now();
|
||||
@@ -436,7 +436,7 @@ impl HealthChecker {
|
||||
);
|
||||
|
||||
self.instance_mgr
|
||||
.run_network_instance(cfg.clone(), ConfigSource::Web)
|
||||
.run_network_instance(cfg.clone(), true, ConfigFileControl::STATIC_CONFIG)
|
||||
.with_context(|| "failed to run network instance")?;
|
||||
self.inst_id_map.insert(node_id, cfg.get_id());
|
||||
|
||||
@@ -497,7 +497,7 @@ impl HealthChecker {
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
// return version, response time on healthy, conn_count
|
||||
) -> anyhow::Result<(String, u64, u32)> {
|
||||
let Some(instance) = instance_mgr.get_network_info(&inst_id) else {
|
||||
let Some(instance) = instance_mgr.get_network_info(&inst_id).await else {
|
||||
anyhow::bail!("healthy check node is not started");
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use api::routes::create_routes;
|
||||
use clap::Parser;
|
||||
use config::AppConfig;
|
||||
use db::{operations::NodeOperations, Db};
|
||||
use easytier::utils::init_logger;
|
||||
use health_checker::HealthChecker;
|
||||
use health_checker_manager::HealthCheckerManager;
|
||||
use std::env;
|
||||
@@ -22,6 +23,11 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::db::cleanup::{CleanupConfig, CleanupManager};
|
||||
|
||||
use mimalloc::MiMalloc;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL_MIMALLOC: MiMalloc = MiMalloc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
@@ -30,24 +36,13 @@ struct Args {
|
||||
admin_password: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// 加载配置
|
||||
let config = AppConfig::default();
|
||||
|
||||
// 初始化日志
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(match config.logging.level.as_str() {
|
||||
"debug" => tracing::Level::DEBUG,
|
||||
"info" => tracing::Level::INFO,
|
||||
"warn" => tracing::Level::WARN,
|
||||
"error" => tracing::Level::ERROR,
|
||||
_ => tracing::Level::INFO,
|
||||
})
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_env_filter(EnvFilter::new("easytier_uptime"))
|
||||
.init();
|
||||
let _ = init_logger(&config.logging, false);
|
||||
|
||||
// 解析命令行参数
|
||||
let args = Args::parse();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum NodeTags {
|
||||
Table,
|
||||
Id,
|
||||
NodeId,
|
||||
Tag,
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum SharedNodes {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 创建 node_tags 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(NodeTags::Table)
|
||||
.if_not_exists()
|
||||
.col(pk_auto(NodeTags::Id).not_null())
|
||||
.col(integer(NodeTags::NodeId).not_null())
|
||||
.col(string(NodeTags::Tag).not_null())
|
||||
.col(
|
||||
timestamp_with_time_zone(NodeTags::CreatedAt)
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_node_tags_node")
|
||||
.from(NodeTags::Table, NodeTags::NodeId)
|
||||
.to(SharedNodes::Table, SharedNodes::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 索引:NodeId
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_node_tags_node")
|
||||
.table(NodeTags::Table)
|
||||
.col(NodeTags::NodeId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 索引:Tag
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_node_tags_tag")
|
||||
.table(NodeTags::Table)
|
||||
.col(NodeTags::Tag)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 唯一索引:每个节点的标签唯一
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("uniq_node_tag_per_node")
|
||||
.table(NodeTags::Table)
|
||||
.col(NodeTags::NodeId)
|
||||
.col(NodeTags::Tag)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 先删除索引
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_node_tags_node")
|
||||
.table(NodeTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_node_tags_tag")
|
||||
.table(NodeTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("uniq_node_tag_per_node")
|
||||
.table(NodeTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(NodeTags::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20250101_000001_create_tables;
|
||||
mod m20250101_000002_create_node_tags;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20250101_000001_create_tables::Migration)]
|
||||
vec![
|
||||
Box::new(m20250101_000001_create_tables::Migration),
|
||||
Box::new(m20250101_000002_create_node_tags::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.4.4",
|
||||
"version": "2.5.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
@@ -13,18 +13,17 @@
|
||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "4.3.3",
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
|
||||
"@tauri-apps/plugin-os": "2.3.0",
|
||||
"@tauri-apps/plugin-process": "2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.3.0",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"aura": "link:@primevue\\themes\\aura",
|
||||
"easytier-frontend-lib": "workspace:*",
|
||||
"ip-num": "1.5.1",
|
||||
"pinia": "^2.2.4",
|
||||
"primevue": "4.3.3",
|
||||
"primevue": "^4.3.9",
|
||||
"tauri-plugin-vpnservice-api": "workspace:*",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
@@ -32,7 +31,7 @@
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.7.3",
|
||||
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
||||
"@primevue/auto-import-resolver": "4.3.3",
|
||||
"@primevue/auto-import-resolver": "4.3.9",
|
||||
"@tauri-apps/api": "2.7.0",
|
||||
"@tauri-apps/cli": "2.7.1",
|
||||
"@types/default-gateway": "^7.2.2",
|
||||
@@ -55,7 +54,7 @@
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
|
||||
Generated
-7220
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.4.4"
|
||||
version = "2.5.0"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -50,8 +50,9 @@ tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-positioner = { version = "2.3.0", features = ["tray-icon"] }
|
||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||
tauri-plugin-os = "2.3.0"
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
|
||||
uuid = "1.17.0"
|
||||
async-trait = "0.1.89"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
@@ -45,10 +45,6 @@
|
||||
"os:allow-arch",
|
||||
"os:allow-hostname",
|
||||
"os:allow-platform",
|
||||
"os:allow-locale",
|
||||
"autostart:default",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-is-enabled"
|
||||
"os:allow-locale"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -7,9 +7,7 @@ use super::Command;
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command as StdCommand, Output};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// The implementation of state check and elevated executing varies on each platform
|
||||
impl Command {
|
||||
@@ -24,8 +22,7 @@ impl Command {
|
||||
/// Prompting the user with a graphical OS dialog for the root password,
|
||||
/// excuting the command with escalated privileges, and return the output
|
||||
pub fn output(&self) -> Result<Output> {
|
||||
let pkexec = PathBuf::from_str("/bin/pkexec")?;
|
||||
let mut command = StdCommand::new(pkexec);
|
||||
let mut command = StdCommand::new("pkexec");
|
||||
let display = env::var("DISPLAY");
|
||||
let xauthority = env::var("XAUTHORITY");
|
||||
let home = env::var("HOME");
|
||||
|
||||
+942
-102
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
fn main() -> std::process::ExitCode {
|
||||
if std::env::args().count() > 1 {
|
||||
app_lib::run_cli()
|
||||
} else {
|
||||
app_lib::run_gui()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,13 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.4.4",
|
||||
"version": "2.5.0",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": "^.+"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"externalBin": [],
|
||||
"resources": [
|
||||
"./wintun.dll",
|
||||
"./Packet.dll"
|
||||
"./Packet.dll",
|
||||
"./*.sys"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
|
||||
Vendored
+54
-32
@@ -9,36 +9,41 @@ declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const MenuItemExit: typeof import('./composables/tray')['MenuItemExit']
|
||||
const MenuItemShow: typeof import('./composables/tray')['MenuItemShow']
|
||||
const ReinitTray: typeof import('./composables/tray')['ReinitTray']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const collectNetworkInfos: typeof import('./composables/network')['collectNetworkInfos']
|
||||
const collectNetworkInfo: typeof import('./composables/backend')['collectNetworkInfo']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const deleteNetworkInstance: typeof import('./composables/backend')['deleteNetworkInstance']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const event2human: typeof import('./composables/utils')['event2human']
|
||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
|
||||
const generateNetworkConfig: typeof import('./composables/backend')['generateNetworkConfig']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getConfig: typeof import('./composables/backend')['getConfig']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
|
||||
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
||||
const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion']
|
||||
const getNetworkMetas: typeof import('./composables/backend')['getNetworkMetas']
|
||||
const getServiceStatus: typeof import('./composables/backend')['getServiceStatus']
|
||||
const h: typeof import('vue')['h']
|
||||
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
|
||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||
const initRpcConnection: typeof import('./composables/backend')['initRpcConnection']
|
||||
const initService: typeof import('./composables/backend')['initService']
|
||||
const initWebClient: typeof import('./composables/backend')['initWebClient']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isAutostart: typeof import('./composables/network')['isAutostart']
|
||||
const isClientRunning: typeof import('./composables/backend')['isClientRunning']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const loadRunningInstanceIdsFromLocalStorage: typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']
|
||||
const isWebClientConnected: typeof import('./composables/backend')['isWebClientConnected']
|
||||
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
|
||||
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
|
||||
const loadMode: typeof import('./composables/mode')['loadMode']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
@@ -46,8 +51,6 @@ declare global {
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
|
||||
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
@@ -57,6 +60,7 @@ declare global {
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onNetworkInstanceChange: typeof import('./composables/mobile_vpn')['onNetworkInstanceChange']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
@@ -64,34 +68,36 @@ declare global {
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
|
||||
const parseNetworkConfig: typeof import('./composables/backend')['parseNetworkConfig']
|
||||
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance']
|
||||
const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance']
|
||||
const runNetworkInstance: typeof import('./composables/backend')['runNetworkInstance']
|
||||
const saveMode: typeof import('./composables/mode')['saveMode']
|
||||
const saveNetworkConfig: typeof import('./composables/backend')['saveNetworkConfig']
|
||||
const sendConfigs: typeof import('./composables/backend')['sendConfigs']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setAutoLaunchStatus: typeof import('./composables/network')['setAutoLaunchStatus']
|
||||
const setLoggingLevel: typeof import('./composables/network')['setLoggingLevel']
|
||||
const setLoggingLevel: typeof import('./composables/backend')['setLoggingLevel']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setServiceStatus: typeof import('./composables/backend')['setServiceStatus']
|
||||
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
||||
const setTrayRunState: typeof import('./composables/tray')['setTrayRunState']
|
||||
const setTrayTooltip: typeof import('./composables/tray')['setTrayTooltip']
|
||||
const setTunFd: typeof import('./composables/network')['setTunFd']
|
||||
const setTunFd: typeof import('./composables/backend')['setTunFd']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const updateNetworkConfigState: typeof import('./composables/backend')['updateNetworkConfigState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
@@ -99,12 +105,12 @@ declare global {
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router/auto')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTray: typeof import('./composables/tray')['useTray']
|
||||
const validateConfig: typeof import('./composables/backend')['validateConfig']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
@@ -116,6 +122,7 @@ declare global {
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
@@ -125,7 +132,7 @@ declare module 'vue' {
|
||||
readonly MenuItemExit: UnwrapRef<typeof import('./composables/tray')['MenuItemExit']>
|
||||
readonly MenuItemShow: UnwrapRef<typeof import('./composables/tray')['MenuItemShow']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly collectNetworkInfos: UnwrapRef<typeof import('./composables/network')['collectNetworkInfos']>
|
||||
readonly collectNetworkInfo: UnwrapRef<typeof import('./composables/backend')['collectNetworkInfo']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
@@ -133,22 +140,32 @@ declare module 'vue' {
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly deleteNetworkInstance: UnwrapRef<typeof import('./composables/backend')['deleteNetworkInstance']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
||||
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/network')['generateNetworkConfig']>
|
||||
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/backend')['generateNetworkConfig']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getConfig: UnwrapRef<typeof import('./composables/backend')['getConfig']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
|
||||
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
||||
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']>
|
||||
readonly getNetworkMetas: UnwrapRef<typeof import('./composables/backend')['getNetworkMetas']>
|
||||
readonly getServiceStatus: UnwrapRef<typeof import('./composables/backend')['getServiceStatus']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly initRpcConnection: UnwrapRef<typeof import('./composables/backend')['initRpcConnection']>
|
||||
readonly initService: UnwrapRef<typeof import('./composables/backend')['initService']>
|
||||
readonly initWebClient: UnwrapRef<typeof import('./composables/backend')['initWebClient']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
|
||||
readonly isClientRunning: UnwrapRef<typeof import('./composables/backend')['isClientRunning']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly isWebClientConnected: UnwrapRef<typeof import('./composables/backend')['isWebClientConnected']>
|
||||
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
|
||||
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
|
||||
readonly loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
@@ -165,6 +182,7 @@ declare module 'vue' {
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onNetworkInstanceChange: UnwrapRef<typeof import('./composables/mobile_vpn')['onNetworkInstanceChange']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
@@ -172,22 +190,25 @@ declare module 'vue' {
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/backend')['parseNetworkConfig']>
|
||||
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/backend')['runNetworkInstance']>
|
||||
readonly saveMode: UnwrapRef<typeof import('./composables/mode')['saveMode']>
|
||||
readonly saveNetworkConfig: UnwrapRef<typeof import('./composables/backend')['saveNetworkConfig']>
|
||||
readonly sendConfigs: UnwrapRef<typeof import('./composables/backend')['sendConfigs']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/backend')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setServiceStatus: UnwrapRef<typeof import('./composables/backend')['setServiceStatus']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
||||
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
||||
readonly setTunFd: UnwrapRef<typeof import('./composables/network')['setTunFd']>
|
||||
readonly setTunFd: UnwrapRef<typeof import('./composables/backend')['setTunFd']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
@@ -198,6 +219,7 @@ declare module 'vue' {
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly updateNetworkConfigState: UnwrapRef<typeof import('./composables/backend')['updateNetworkConfigState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
@@ -205,15 +227,15 @@ declare module 'vue' {
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
|
||||
readonly validateConfig: UnwrapRef<typeof import('./composables/backend')['validateConfig']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { getEasytierVersion } from '~/composables/network'
|
||||
import { getEasytierVersion } from '~/composables/backend'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import type { Mode, ServiceMode, RemoteMode } from '~/composables/mode';
|
||||
import { appConfigDir, appLogDir } from '@tauri-apps/api/path';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const model = defineModel<Mode>({ required: true })
|
||||
const emit = defineEmits(['uninstall-service', 'stop-service'])
|
||||
|
||||
const defaultConfigDir = ref('')
|
||||
const defaultLogDir = ref('')
|
||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
||||
const isServiceStatusLoaded = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
defaultConfigDir.value = await join(await appConfigDir(), 'config.d')
|
||||
defaultLogDir.value = await appLogDir()
|
||||
})
|
||||
|
||||
const modeOptions = computed(() => [
|
||||
{ label: t('mode.normal'), value: 'normal' },
|
||||
{ label: t('mode.service'), value: 'service' },
|
||||
{ label: t('mode.remote'), value: 'remote' },
|
||||
]);
|
||||
|
||||
const serviceMode = computed({
|
||||
get: () => model.value.mode === 'service' ? model.value as ServiceMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const remoteMode = computed({
|
||||
get: () => model.value.mode === 'remote' ? model.value as RemoteMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const statusColorClass = computed(() => {
|
||||
switch (serviceStatus.value) {
|
||||
case 'Running':
|
||||
return 'text-green-600'
|
||||
case 'Stopped':
|
||||
return 'text-orange-600'
|
||||
case 'NotInstalled':
|
||||
return 'text-gray-600'
|
||||
default:
|
||||
return 'text-gray-600'
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => model.value.mode, async (newMode, oldMode) => {
|
||||
if (newMode === oldMode)
|
||||
return
|
||||
|
||||
if (newMode === 'service' && !isServiceStatusLoaded.value) {
|
||||
serviceStatus.value = await getServiceStatus()
|
||||
isServiceStatusLoaded.value = true
|
||||
}
|
||||
|
||||
const oldModelValue = { ...model.value }
|
||||
|
||||
if (newMode === 'normal') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'normal',
|
||||
}
|
||||
}
|
||||
else if (newMode === 'service') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'service',
|
||||
config_dir: serviceMode.value?.config_dir || defaultConfigDir.value,
|
||||
rpc_portal: serviceMode.value?.rpc_portal || '127.0.0.1:15999',
|
||||
file_log_level: serviceMode.value?.file_log_level || 'off',
|
||||
file_log_dir: serviceMode.value?.file_log_dir || defaultLogDir.value,
|
||||
}
|
||||
}
|
||||
else if (newMode === 'remote') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'remote',
|
||||
remote_rpc_address: remoteMode.value?.remote_rpc_address || 'tcp://127.0.0.1:15999',
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<SelectButton id="mode-select" v-model="model.mode" :options="modeOptions" option-label="label"
|
||||
option-value="value" fluid />
|
||||
</div>
|
||||
|
||||
<!-- Mode descriptions -->
|
||||
<div v-if="model.mode === 'normal'" class="text-sm text-gray-500">
|
||||
{{ t('mode.normal_description') }}
|
||||
</div>
|
||||
<div v-else-if="model.mode === 'service'" class="text-sm text-gray-500">
|
||||
{{ t('mode.service_description') }}
|
||||
</div>
|
||||
<div v-else-if="model.mode === 'remote'" class="text-sm text-gray-500">
|
||||
{{ t('mode.remote_description') }}
|
||||
</div>
|
||||
|
||||
<div v-if="serviceMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="config-dir">{{ t('mode.config_dir') }}</label>
|
||||
<InputText id="config-dir" v-model="serviceMode.config_dir" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="rpc-portal">{{ t('mode.rpc_portal') }}</label>
|
||||
<InputText id="rpc-portal" v-model="serviceMode.rpc_portal" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-level">{{ t('mode.log_level') }}</label>
|
||||
<Select id="log-level" v-model="serviceMode.file_log_level"
|
||||
:options="['off', 'warn', 'info', 'debug', 'trace']" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-dir">{{ t('mode.log_dir') }}</label>
|
||||
<InputText id="log-dir" v-model="serviceMode.file_log_dir" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<label>{{ t('mode.service_status') }}</label>
|
||||
<span :class="statusColorClass">{{ t(`mode.service_status_${serviceStatus.toLowerCase()}`) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button :label="t('mode.stop_service')" icon="pi pi-stop-circle" v-if="serviceStatus === 'Running'"
|
||||
@click="emit('stop-service')" severity="warn" text />
|
||||
<Button :label="t('mode.uninstall_service')" icon="pi pi-trash" v-if="serviceStatus !== 'NotInstalled'"
|
||||
@click="emit('uninstall-service')" severity="danger" text />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="remoteMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="remote-addr">{{ t('mode.remote_rpc_address') }}</label>
|
||||
<InputText id="remote-addr" v-model="remoteMode.remote_rpc_address" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Api, type NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
||||
|
||||
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||
interface ServiceOptions {
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
file_log_level: string
|
||||
file_log_dir: string
|
||||
config_server?: string
|
||||
}
|
||||
|
||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
}
|
||||
|
||||
export async function generateNetworkConfig(tomlConfig: string) {
|
||||
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
|
||||
}
|
||||
|
||||
export async function runNetworkInstance(cfg: NetworkConfig, save: boolean) {
|
||||
return invoke('run_network_instance', { cfg, save })
|
||||
}
|
||||
|
||||
export async function collectNetworkInfo(instanceId: string) {
|
||||
return await invoke<Api.CollectNetworkInfoResponse>('collect_network_info', { instanceId })
|
||||
}
|
||||
|
||||
export async function setLoggingLevel(level: string) {
|
||||
return await invoke('set_logging_level', { level })
|
||||
}
|
||||
|
||||
export async function setTunFd(fd: number) {
|
||||
return await invoke('set_tun_fd', { fd })
|
||||
}
|
||||
|
||||
export async function getEasytierVersion() {
|
||||
return await invoke<string>('easytier_version')
|
||||
}
|
||||
|
||||
export async function listNetworkInstanceIds() {
|
||||
return await invoke<ListNetworkInstanceIdResponse>('list_network_instance_ids')
|
||||
}
|
||||
|
||||
export async function deleteNetworkInstance(instanceId: string) {
|
||||
return await invoke('remove_network_instance', { instanceId })
|
||||
}
|
||||
|
||||
export async function updateNetworkConfigState(instanceId: string, disabled: boolean) {
|
||||
return await invoke('update_network_config_state', { instanceId, disabled })
|
||||
}
|
||||
|
||||
export async function saveNetworkConfig(cfg: NetworkConfig) {
|
||||
return await invoke('save_network_config', { cfg })
|
||||
}
|
||||
|
||||
export async function validateConfig(cfg: NetworkConfig) {
|
||||
return await invoke<ValidateConfigResponse>('validate_config', { cfg })
|
||||
}
|
||||
|
||||
export async function getConfig(instanceId: string) {
|
||||
return await invoke<NetworkConfig>('get_config', { instanceId })
|
||||
}
|
||||
|
||||
export async function sendConfigs(enabledNetworks: string[]) {
|
||||
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
||||
return await invoke('load_configs', { configs: networkList, enabledNetworks })
|
||||
}
|
||||
|
||||
export async function getNetworkMetas(instanceIds: string[]) {
|
||||
return await invoke<GetNetworkMetasResponse>('get_network_metas', { instanceIds })
|
||||
}
|
||||
|
||||
export async function initService(opts?: ServiceOptions) {
|
||||
return await invoke('init_service', { opts })
|
||||
}
|
||||
|
||||
export async function setServiceStatus(enable: boolean) {
|
||||
return await invoke('set_service_status', { enable })
|
||||
}
|
||||
|
||||
export async function getServiceStatus() {
|
||||
return await invoke<ServiceStatus>('get_service_status')
|
||||
}
|
||||
|
||||
export async function initRpcConnection(url?: string) {
|
||||
return await invoke('init_rpc_connection', { url })
|
||||
}
|
||||
|
||||
export async function isClientRunning() {
|
||||
return await invoke<boolean>('is_client_running')
|
||||
}
|
||||
|
||||
export async function initWebClient(url?: string) {
|
||||
return await invoke('init_web_client', { url })
|
||||
}
|
||||
|
||||
export async function isWebClientConnected() {
|
||||
return await invoke<boolean>('is_web_client_connected')
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Event, listen } from "@tauri-apps/api/event";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { NetworkTypes } from "easytier-frontend-lib"
|
||||
|
||||
const EVENTS = Object.freeze({
|
||||
SAVE_CONFIGS: 'save_configs',
|
||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
|
||||
VPN_SERVICE_STOP: 'vpn_service_stop',
|
||||
DHCP_IP_CHANGED: 'dhcp_ip_changed',
|
||||
PROXY_CIDRS_UPDATED: 'proxy_cidrs_updated',
|
||||
EVENT_LAGGED: 'event_lagged',
|
||||
});
|
||||
|
||||
function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
||||
localStorage.setItem('networkList', JSON.stringify(event.payload));
|
||||
}
|
||||
|
||||
async function onPreRunNetworkInstance(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onPostRunNetworkInstance(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(event: Event<string>) {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
|
||||
async function onDhcpIpChanged(event: Event<string>) {
|
||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${event.payload}`);
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onProxyCidrsUpdated(event: Event<string>) {
|
||||
console.log(`Received event '${EVENTS.PROXY_CIDRS_UPDATED}' for instance: ${event.payload}`);
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onEventLagged(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listenGlobalEvents() {
|
||||
const unlisteners = [
|
||||
await listen(EVENTS.SAVE_CONFIGS, onSaveConfigs),
|
||||
await listen(EVENTS.PRE_RUN_NETWORK_INSTANCE, onPreRunNetworkInstance),
|
||||
await listen(EVENTS.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
|
||||
await listen(EVENTS.VPN_SERVICE_STOP, onVpnServiceStop),
|
||||
await listen(EVENTS.DHCP_IP_CHANGED, onDhcpIpChanged),
|
||||
await listen(EVENTS.PROXY_CIDRS_UPDATED, onProxyCidrsUpdated),
|
||||
await listen(EVENTS.EVENT_LAGGED, onEventLagged),
|
||||
];
|
||||
|
||||
return () => {
|
||||
unlisteners.forEach(unlisten => unlisten());
|
||||
};
|
||||
}
|
||||
@@ -5,20 +5,23 @@ import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
|
||||
type Route = NetworkTypes.Route
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
interface vpnStatus {
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
dns: string | null | undefined
|
||||
}
|
||||
|
||||
let dhcpPollingTimer: NodeJS.Timeout | null = null
|
||||
const DHCP_POLLING_INTERVAL = 2000 // 2秒后重试
|
||||
|
||||
const curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Cidr: undefined,
|
||||
routes: [],
|
||||
dns: undefined,
|
||||
}
|
||||
|
||||
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
@@ -42,17 +45,19 @@ async function doStopVpn() {
|
||||
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
}
|
||||
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?: string) {
|
||||
if (curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('start vpn service', ipv4Addr, cidr, routes)
|
||||
console.log('start vpn service', ipv4Addr, cidr, routes, dns)
|
||||
const start_ret = await start_vpn({
|
||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
||||
routes,
|
||||
dns,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
mtu: 1300,
|
||||
})
|
||||
@@ -63,13 +68,14 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.dns = dns
|
||||
}
|
||||
|
||||
async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
|
||||
setTunFd(payload.fd)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +99,7 @@ async function registerVpnServiceListener() {
|
||||
)
|
||||
}
|
||||
|
||||
function getRoutesForVpn(routes: Route[]): string[] {
|
||||
function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfig): string[] {
|
||||
if (!routes) {
|
||||
return []
|
||||
}
|
||||
@@ -108,30 +114,50 @@ function getRoutesForVpn(routes: Route[]): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
node_config.routes.forEach(r => {
|
||||
ret.push(r)
|
||||
})
|
||||
|
||||
if (node_config.enable_magic_dns) {
|
||||
ret.push('100.100.100.101/32')
|
||||
}
|
||||
|
||||
// sort and dedup
|
||||
return Array.from(new Set(ret)).sort()
|
||||
}
|
||||
|
||||
async function onNetworkInstanceChange() {
|
||||
console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
|
||||
const insts = networkStore.networkInstanceIds
|
||||
const no_tun = networkStore.isNoTunEnabled(insts[0])
|
||||
if (no_tun) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
if (!insts) {
|
||||
await doStopVpn()
|
||||
return
|
||||
export async function onNetworkInstanceChange(instanceId: string) {
|
||||
console.error('vpn service network instance change id', instanceId)
|
||||
|
||||
if (dhcpPollingTimer) {
|
||||
clearTimeout(dhcpPollingTimer)
|
||||
dhcpPollingTimer = null
|
||||
}
|
||||
|
||||
const curNetworkInfo = networkStore.networkInfos[insts[0]]
|
||||
if (!instanceId) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
const config = await getConfig(instanceId)
|
||||
if (config.no_tun) {
|
||||
return
|
||||
}
|
||||
const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
||||
|
||||
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
||||
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
||||
dhcpPollingTimer = setTimeout(() => {
|
||||
onNetworkInstanceChange(instanceId)
|
||||
}, DHCP_POLLING_INTERVAL)
|
||||
return
|
||||
}
|
||||
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
@@ -142,12 +168,15 @@ async function onNetworkInstanceChange() {
|
||||
network_length = 24
|
||||
}
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
|
||||
|
||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined;
|
||||
|
||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
const dnsChanged = dns != curVpnStatus.dns
|
||||
|
||||
if (ipChanged || routesChanged) {
|
||||
if (ipChanged || routesChanged || dnsChanged) {
|
||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
try {
|
||||
await doStopVpn()
|
||||
@@ -157,51 +186,28 @@ async function onNetworkInstanceChange() {
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, 24, routes)
|
||||
await doStartVpn(virtual_ip, network_length, routes, dns)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('start vpn service failed, clear all network insts.', e)
|
||||
networkStore.clearNetworkInstances()
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
console.error('start vpn service failed, stop all other network insts.', e)
|
||||
await runNetworkInstance(config, true); //on android config should always be saved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function watchNetworkInstance() {
|
||||
let subscribe_running = false
|
||||
networkStore.$subscribe(async () => {
|
||||
if (subscribe_running) {
|
||||
return
|
||||
}
|
||||
subscribe_running = true
|
||||
try {
|
||||
await onNetworkInstanceChange()
|
||||
}
|
||||
catch (_) {
|
||||
}
|
||||
subscribe_running = false
|
||||
})
|
||||
console.error('vpn service watch network instance')
|
||||
}
|
||||
|
||||
function isNoTunEnabled(instanceId: string | undefined) {
|
||||
async function isNoTunEnabled(instanceId: string | undefined) {
|
||||
if (!instanceId) {
|
||||
return false
|
||||
}
|
||||
const no_tun = networkStore.isNoTunEnabled(instanceId)
|
||||
if (no_tun) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return (await getConfig(instanceId)).no_tun ?? false
|
||||
}
|
||||
|
||||
export async function initMobileVpnService() {
|
||||
await registerVpnServiceListener()
|
||||
await watchNetworkInstance()
|
||||
}
|
||||
|
||||
export async function prepareVpnService(instanceId: string) {
|
||||
if (isNoTunEnabled(instanceId)) {
|
||||
if (await isNoTunEnabled(instanceId)) {
|
||||
return
|
||||
}
|
||||
console.log('prepare vpn')
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
export interface WebClientConfig {
|
||||
config_server_url?: string
|
||||
}
|
||||
|
||||
interface NormalMode extends WebClientConfig {
|
||||
mode: 'normal'
|
||||
}
|
||||
|
||||
export interface ServiceMode extends WebClientConfig {
|
||||
mode: 'service'
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||
file_log_dir: string
|
||||
}
|
||||
|
||||
export interface RemoteMode {
|
||||
mode: 'remote'
|
||||
remote_rpc_address: string
|
||||
}
|
||||
|
||||
export function saveMode(mode: Mode) {
|
||||
localStorage.setItem('app_mode', JSON.stringify(mode))
|
||||
}
|
||||
|
||||
|
||||
export function loadMode(): Mode {
|
||||
const modeStr = localStorage.getItem('app_mode')
|
||||
if (modeStr) {
|
||||
let mode = JSON.parse(modeStr) as Mode
|
||||
if (type() === 'android') {
|
||||
return { ...mode, mode: 'normal' }
|
||||
}
|
||||
return mode
|
||||
} else {
|
||||
return { mode: 'normal' }
|
||||
}
|
||||
}
|
||||
|
||||
export type Mode = NormalMode | ServiceMode | RemoteMode
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type NetworkInstanceRunningInfo = NetworkTypes.NetworkInstanceRunningInfo
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
}
|
||||
|
||||
export async function generateNetworkConfig(tomlConfig: string) {
|
||||
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
|
||||
}
|
||||
|
||||
export async function runNetworkInstance(cfg: NetworkConfig) {
|
||||
return invoke('run_network_instance', { cfg })
|
||||
}
|
||||
|
||||
export async function retainNetworkInstance(instanceIds: string[]) {
|
||||
return invoke('retain_network_instance', { instanceIds })
|
||||
}
|
||||
|
||||
export async function collectNetworkInfos() {
|
||||
return await invoke<Record<string, NetworkInstanceRunningInfo>>('collect_network_infos')
|
||||
}
|
||||
|
||||
export async function getOsHostname() {
|
||||
return await invoke<string>('get_os_hostname')
|
||||
}
|
||||
|
||||
export async function isAutostart() {
|
||||
return await invoke<boolean>('is_autostart')
|
||||
}
|
||||
|
||||
export async function setLoggingLevel(level: string) {
|
||||
return await invoke('set_logging_level', { level })
|
||||
}
|
||||
|
||||
export async function setTunFd(instanceId: string, fd: number) {
|
||||
return await invoke('set_tun_fd', { instanceId, fd })
|
||||
}
|
||||
|
||||
export async function getEasytierVersion() {
|
||||
return await invoke<string>('easytier_version')
|
||||
}
|
||||
+13
-12
@@ -1,15 +1,15 @@
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import PrimeVue from 'primevue/config';
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import App from '~/App.vue'
|
||||
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
|
||||
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib';
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto';
|
||||
import { routes } from 'vue-router/auto-routes';
|
||||
import App from '~/App.vue';
|
||||
|
||||
import 'easytier-frontend-lib/style.css';
|
||||
import { ConfirmationService, DialogService, ToastService } from 'primevue';
|
||||
import '~/styles.css';
|
||||
|
||||
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||
import '~/styles.css'
|
||||
import 'easytier-frontend-lib/style.css'
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
@@ -29,7 +29,6 @@ if (import.meta.env.PROD) {
|
||||
|
||||
async function main() {
|
||||
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -55,7 +54,9 @@ async function main() {
|
||||
},
|
||||
},
|
||||
})
|
||||
app.use(ToastService as any)
|
||||
app.use(ToastService)
|
||||
app.use(DialogService)
|
||||
app.use(ConfirmationService)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { type Api, type NetworkTypes } from "easytier-frontend-lib";
|
||||
import * as backend from "~/composables/backend";
|
||||
|
||||
export class GUIRemoteClient implements Api.RemoteClient {
|
||||
async validate_config(config: NetworkTypes.NetworkConfig): Promise<Api.ValidateConfigResponse> {
|
||||
return backend.validateConfig(config);
|
||||
}
|
||||
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
|
||||
await backend.runNetworkInstance(config, save);
|
||||
}
|
||||
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {
|
||||
return backend.collectNetworkInfo(inst_id).then(infos => infos.info.map[inst_id]);
|
||||
}
|
||||
async list_network_instance_ids(): Promise<Api.ListNetworkInstanceIdResponse> {
|
||||
return backend.listNetworkInstanceIds();
|
||||
}
|
||||
async delete_network(inst_id: string): Promise<undefined> {
|
||||
await backend.deleteNetworkInstance(inst_id);
|
||||
}
|
||||
async update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined> {
|
||||
await backend.updateNetworkConfigState(inst_id, disabled);
|
||||
}
|
||||
async save_config(config: NetworkTypes.NetworkConfig): Promise<undefined> {
|
||||
await backend.saveNetworkConfig(config);
|
||||
}
|
||||
async get_network_config(inst_id: string): Promise<NetworkTypes.NetworkConfig> {
|
||||
return backend.getConfig(inst_id);
|
||||
}
|
||||
async generate_config(config: NetworkTypes.NetworkConfig): Promise<Api.GenerateConfigResponse> {
|
||||
try {
|
||||
return { toml_config: await backend.parseNetworkConfig(config) };
|
||||
} catch (e) {
|
||||
return { error: e + "" };
|
||||
}
|
||||
}
|
||||
async parse_config(toml_config: string): Promise<Api.ParseConfigResponse> {
|
||||
try {
|
||||
return { config: await backend.generateNetworkConfig(toml_config) }
|
||||
} catch (e) {
|
||||
return { error: e + "" };
|
||||
}
|
||||
}
|
||||
async get_network_metas(instance_ids: string[]): Promise<Api.GetNetworkMetasResponse> {
|
||||
return await backend.getNetworkMetas(instance_ids);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
|
||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
if (target_enable) {
|
||||
await enable()
|
||||
}
|
||||
else {
|
||||
// 消除没有配置自启动时进行关闭操作报错
|
||||
try {
|
||||
await disable()
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
||||
return isEnabled()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoLaunchStatusAsync(): boolean {
|
||||
return localStorage.getItem('auto_launch') === 'true'
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export async function loadDockVisibilityAsync(visible: boolean): Promise<boolean> {
|
||||
try {
|
||||
await invoke('set_dock_visibility', { visible })
|
||||
localStorage.setItem('dock_visibility', JSON.stringify(visible))
|
||||
return visible
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to set dock visibility:', e)
|
||||
return getDockVisibilityStatus()
|
||||
}
|
||||
}
|
||||
|
||||
export function getDockVisibilityStatus(): boolean {
|
||||
const stored = localStorage.getItem('dock_visibility')
|
||||
return stored !== null ? JSON.parse(stored) : true
|
||||
}
|
||||
+364
-295
@@ -1,148 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { appLogDir } from '@tauri-apps/api/path'
|
||||
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from 'easytier-frontend-lib'
|
||||
|
||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import { I18nUtils, RemoteManagement, Utils } from "easytier-frontend-lib"
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
import { getDockVisibilityStatus, loadDockVisibilityAsync } from '~/modules/dock_visibility'
|
||||
import { GUIRemoteClient } from '~/modules/api'
|
||||
|
||||
import { useToast, useConfirm } from 'primevue'
|
||||
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||
import { getServiceStatus } from '~/composables/backend'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const visible = ref(false)
|
||||
const confirm = useConfirm()
|
||||
const aboutVisible = ref(false)
|
||||
const tomlConfig = ref('')
|
||||
const modeDialogVisible = ref(false)
|
||||
const currentMode = ref<Mode>({ mode: 'normal' })
|
||||
const editingMode = ref<Mode>({ mode: 'normal' })
|
||||
const isModeSaving = ref(false)
|
||||
const manualDisconnect = ref(false)
|
||||
|
||||
const configServerDialogVisible = ref(false)
|
||||
const configServerConnected = ref(false)
|
||||
|
||||
async function openModeDialog() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||
modeDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onModeSave() {
|
||||
if (isModeSaving.value) {
|
||||
return;
|
||||
}
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await initWithMode(editingMode.value);
|
||||
modeDialogVisible.value = false
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error switching mode", e, currentMode.value, editingMode.value)
|
||||
await initWithMode(currentMode.value);
|
||||
}
|
||||
finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onUninstallService() {
|
||||
confirm.require({
|
||||
message: t('mode.uninstall_service_confirm'),
|
||||
header: t('mode.uninstall_service'),
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectProps: {
|
||||
label: t('web.common.cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: t('mode.uninstall_service'),
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await initWithMode({ ...currentMode.value, mode: 'normal' });
|
||||
await initService(undefined)
|
||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.uninstall_service_success'), life: 3000 })
|
||||
modeDialogVisible.value = false
|
||||
} catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error uninstalling service", e)
|
||||
} finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onStopService() {
|
||||
isModeSaving.value = true
|
||||
manualDisconnect.value = true
|
||||
try {
|
||||
await setServiceStatus(false)
|
||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.stop_service_success'), life: 3000 })
|
||||
modeDialogVisible.value = false
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error stopping service", e)
|
||||
}
|
||||
finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initWithMode(mode: Mode) {
|
||||
const running_inst_ids = (await remoteClient.value.list_network_instance_ids().catch(() => undefined))?.running_inst_ids ?? []
|
||||
|
||||
if (currentMode.value.mode === 'service' && mode.mode !== 'service') {
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "Running") {
|
||||
manualDisconnect.value = true
|
||||
await setServiceStatus(false)
|
||||
serviceStatus = await getServiceStatus()
|
||||
for (let i = 0; i < 10; i++) { // macOS takes a while to stop the service
|
||||
if (serviceStatus === "Stopped") {
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
await initService(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
let url: string | undefined = undefined
|
||||
let retrys = 1
|
||||
switch (mode.mode) {
|
||||
case 'remote':
|
||||
if (!mode.remote_rpc_address) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.remote_rpc_address_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
url = mode.remote_rpc_address
|
||||
break;
|
||||
case 'service':
|
||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||
mode.config_server_url = mode.config_server_url || undefined
|
||||
await initService({
|
||||
config_dir: mode.config_dir,
|
||||
file_log_dir: mode.file_log_dir,
|
||||
file_log_level: mode.file_log_level,
|
||||
rpc_portal: mode.rpc_portal,
|
||||
config_server: mode.config_server_url,
|
||||
})
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
await setServiceStatus(true)
|
||||
}
|
||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||
retrys = 5
|
||||
break;
|
||||
}
|
||||
for (let i = 0; i < retrys; i++) {
|
||||
try {
|
||||
await connectRpcClient(url)
|
||||
break;
|
||||
} catch (e) {
|
||||
if (i === retrys - 1) {
|
||||
throw e;
|
||||
}
|
||||
console.error("Error connecting rpc client, retrying...", e)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
await sendConfigs(running_inst_ids.map(Utils.UuidToStr))
|
||||
if (mode.mode === 'normal') {
|
||||
mode.config_server_url = mode.config_server_url || undefined
|
||||
initWebClient(mode.config_server_url)
|
||||
}
|
||||
currentMode.value = mode
|
||||
saveMode(mode)
|
||||
clientRunning.value = await isClientRunning()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentMode.value = loadMode()
|
||||
initWithMode(currentMode.value);
|
||||
});
|
||||
|
||||
useTray(true)
|
||||
let toast = useToast();
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: () => activeStep.value == "2" ? t('show_config') : t('edit_config'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: async () => {
|
||||
try {
|
||||
const ret = await parseNetworkConfig(networkStore.curNetwork)
|
||||
tomlConfig.value = ret
|
||||
}
|
||||
catch (e: any) {
|
||||
tomlConfig.value = e
|
||||
}
|
||||
visible.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => t('del_cur_network'),
|
||||
icon: 'pi pi-times',
|
||||
command: async () => {
|
||||
networkStore.removeNetworkInstance(networkStore.curNetwork.instance_id)
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
networkStore.delCurNetwork()
|
||||
},
|
||||
disabled: () => networkStore.networkList.length <= 1,
|
||||
},
|
||||
])
|
||||
const remoteClient = computed(() => new GUIRemoteClient());
|
||||
const instanceId = ref<string | undefined>(undefined);
|
||||
const clientRunning = ref(false);
|
||||
|
||||
enum Severity {
|
||||
None = 'none',
|
||||
Success = 'success',
|
||||
Info = 'info',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
const messageBarSeverity = ref(Severity.None)
|
||||
const messageBarContent = ref('')
|
||||
const toast = useToast()
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
const curNetworkConfig = computed(() => {
|
||||
if (networkStore.curNetworkId) {
|
||||
// console.log('instanceId', props.instanceId)
|
||||
const c = networkStore.networkList.find(n => n.instance_id === networkStore.curNetworkId)
|
||||
if (c !== undefined)
|
||||
return c
|
||||
}
|
||||
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const curNetworkInst = computed<NetworkTypes.NetworkInstance | null>(() => {
|
||||
let ret = networkStore.networkInstances.find(n => n.instance_id === curNetworkConfig.value.instance_id)
|
||||
console.log('curNetworkInst', ret)
|
||||
if (ret === undefined) {
|
||||
return null;
|
||||
} else {
|
||||
return ret;
|
||||
watch(clientRunning, async (newVal, oldVal) => {
|
||||
if (!newVal && oldVal) {
|
||||
if (manualDisconnect.value) {
|
||||
manualDisconnect.value = false
|
||||
return
|
||||
}
|
||||
await reconnectClient()
|
||||
}
|
||||
})
|
||||
|
||||
function addNewNetwork() {
|
||||
networkStore.addNewNetwork()
|
||||
networkStore.curNetwork = networkStore.lastNetwork
|
||||
}
|
||||
|
||||
networkStore.$subscribe(async () => {
|
||||
networkStore.saveToLocalStorage()
|
||||
try {
|
||||
await parseNetworkConfig(networkStore.curNetwork)
|
||||
messageBarSeverity.value = Severity.None
|
||||
}
|
||||
catch (e: any) {
|
||||
messageBarContent.value = e
|
||||
messageBarSeverity.value = Severity.Error
|
||||
}
|
||||
})
|
||||
|
||||
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService(cfg.instance_id)
|
||||
networkStore.clearNetworkInstances()
|
||||
}
|
||||
else {
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
}
|
||||
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
networkStore.addNetworkInstance(cfg.instance_id)
|
||||
|
||||
try {
|
||||
await runNetworkInstance(cfg)
|
||||
networkStore.addAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
// console.error(e)
|
||||
toast.add({ severity: 'info', detail: e })
|
||||
}
|
||||
|
||||
cb()
|
||||
}
|
||||
|
||||
async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
||||
// console.log('stopNetworkCb', cfg, cb)
|
||||
cb()
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
networkStore.removeAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
|
||||
async function updateNetworkInfos() {
|
||||
networkStore.updateWithNetworkInfos(await collectNetworkInfos())
|
||||
}
|
||||
|
||||
let intervalId = 0
|
||||
onMounted(async () => {
|
||||
intervalId = window.setInterval(async () => {
|
||||
await updateNetworkInfos()
|
||||
}, 500)
|
||||
clientRunning.value = await isClientRunning().catch(() => false)
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
clientRunning.value = await isClientRunning()
|
||||
} catch (e) {
|
||||
clientRunning.value = false
|
||||
console.error("Error checking client running status", e)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
async function reconnectClient() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()));
|
||||
await onModeSave()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.setTimeout(async () => {
|
||||
await setTrayMenu([
|
||||
await MenuItemShow(t('tray.show')),
|
||||
@@ -150,16 +228,53 @@ onMounted(async () => {
|
||||
])
|
||||
}, 1000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(intervalId))
|
||||
|
||||
const activeStep = computed(() => {
|
||||
return networkStore.networkInstanceIds.includes(networkStore.curNetworkId) ? '2' : '1'
|
||||
})
|
||||
|
||||
let current_log_level = 'off'
|
||||
|
||||
const setting_menu = ref()
|
||||
const setting_menu_items = ref([
|
||||
const log_menu = ref()
|
||||
// 从后端获取正确的日志路径
|
||||
async function getLogDirPath(): Promise<string> {
|
||||
return await invoke<string>('get_log_dir_path')
|
||||
}
|
||||
|
||||
const log_menu_items_popup: Ref<MenuItem[]> = ref([
|
||||
...['off', 'warn', 'info', 'debug', 'trace'].map(level => ({
|
||||
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
|
||||
command: async () => {
|
||||
current_log_level = level
|
||||
await setLoggingLevel(level)
|
||||
},
|
||||
})),
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
label: () => t('logging_open_dir'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: async () => {
|
||||
// console.log('open log dir', await getLogDirPath())
|
||||
await open(await getLogDirPath())
|
||||
},
|
||||
visible: () => type() !== 'android',
|
||||
},
|
||||
{
|
||||
label: () => t('logging_copy_dir'),
|
||||
icon: 'pi pi-tablet',
|
||||
command: async () => {
|
||||
await writeText(await getLogDirPath())
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
function toggle_log_menu(event: any) {
|
||||
log_menu.value.toggle(event)
|
||||
}
|
||||
|
||||
function getLabel(item: MenuItem) {
|
||||
return typeof item.label === 'function' ? item.label() : item.label
|
||||
}
|
||||
|
||||
const setting_menu_items: Ref<MenuItem[]> = ref([
|
||||
{
|
||||
label: () => t('exchange_language'),
|
||||
icon: 'pi pi-language',
|
||||
@@ -172,55 +287,22 @@ const setting_menu_items = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => getAutoLaunchStatus() ? t('disable_auto_launch') : t('enable_auto_launch'),
|
||||
icon: 'pi pi-desktop',
|
||||
command: async () => {
|
||||
await loadAutoLaunchStatusAsync(!getAutoLaunchStatus())
|
||||
},
|
||||
label: () => `${t('mode.switch_mode')}: ${t('mode.' + currentMode.value.mode)}`,
|
||||
icon: 'pi pi-sync',
|
||||
command: openModeDialog,
|
||||
visible: () => type() !== 'android',
|
||||
},
|
||||
{
|
||||
label: () => getDockVisibilityStatus() ? t('hide_dock_icon') : t('show_dock_icon'),
|
||||
icon: 'pi pi-eye-slash',
|
||||
command: async () => {
|
||||
await loadDockVisibilityAsync(!getDockVisibilityStatus())
|
||||
},
|
||||
visible: () => type() === 'macos',
|
||||
label: () => `${t('config-server.title')}${t('config-server.' + configServerConnectionStatus.value)}`,
|
||||
icon: 'pi pi-globe',
|
||||
command: openConfigServerDialog,
|
||||
visible: () => ["normal", "service"].includes(currentMode.value.mode),
|
||||
},
|
||||
{
|
||||
key: 'logging_menu',
|
||||
label: () => t('logging'),
|
||||
icon: 'pi pi-file',
|
||||
items: (function () {
|
||||
const levels = ['off', 'warn', 'info', 'debug', 'trace']
|
||||
const items = []
|
||||
for (const level of levels) {
|
||||
items.push({
|
||||
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
|
||||
command: async () => {
|
||||
current_log_level = level
|
||||
await setLoggingLevel(level)
|
||||
},
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
separator: true,
|
||||
})
|
||||
items.push({
|
||||
label: () => t('logging_open_dir'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: async () => {
|
||||
// console.log('open log dir', await appLogDir())
|
||||
await open(await appLogDir())
|
||||
},
|
||||
})
|
||||
items.push({
|
||||
label: () => t('logging_copy_dir'),
|
||||
icon: 'pi pi-tablet',
|
||||
command: async () => {
|
||||
await writeText(await appLogDir())
|
||||
},
|
||||
})
|
||||
return items
|
||||
})(),
|
||||
items: [], // Keep this to show it's a parent menu
|
||||
},
|
||||
{
|
||||
label: () => t('about.title'),
|
||||
@@ -238,25 +320,11 @@ const setting_menu_items = ref([
|
||||
},
|
||||
])
|
||||
|
||||
function toggle_setting_menu(event: any) {
|
||||
setting_menu.value.toggle(event)
|
||||
async function connectRpcClient(url?: string) {
|
||||
await initRpcConnection(url)
|
||||
console.log("easytier rpc connection established")
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
networkStore.loadFromLocalStorage()
|
||||
if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
|
||||
getCurrentWindow().hide()
|
||||
const autoStartIds = networkStore.autoStartInstIds
|
||||
for (const id of autoStartIds) {
|
||||
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
|
||||
if (cfg) {
|
||||
networkStore.addNetworkInstance(cfg.instance_id)
|
||||
await runNetworkInstance(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
@@ -266,123 +334,124 @@ onMounted(async () => {
|
||||
console.error("easytier init vpn service failed", e)
|
||||
}
|
||||
}
|
||||
const unlisten = await listenGlobalEvents()
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
})
|
||||
|
||||
function isRunning(id: string) {
|
||||
return networkStore.networkInstanceIds.includes(id)
|
||||
async function openConfigServerDialog() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||
configServerDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveTomlConfig(tomlConfig: string) {
|
||||
const config = await generateNetworkConfig(tomlConfig)
|
||||
networkStore.replaceCurNetwork(config);
|
||||
toast.add({ severity: 'success', detail: t('config_saved'), life: 3000 })
|
||||
visible.value = false
|
||||
async function onConfigServerSave() {
|
||||
if (JSON.stringify(currentMode.value) === JSON.stringify(editingMode.value)) {
|
||||
configServerDialogVisible.value = false
|
||||
return;
|
||||
}
|
||||
if (editingMode.value.mode === 'service') {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
confirm.require({
|
||||
message: t('config-server.update_service_confirm'),
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectProps: {
|
||||
label: t('web.common.cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: t('web.common.confirm'),
|
||||
},
|
||||
accept: async () => {
|
||||
resolve()
|
||||
},
|
||||
reject: () => {
|
||||
reject()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
console.log("Saving config server url", (editingMode.value as WebClientConfig).config_server_url)
|
||||
await onModeSave();
|
||||
configServerDialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
onMounted(() => {
|
||||
const timer = setInterval(async () => {
|
||||
if (currentMode.value.mode !== 'normal') return;
|
||||
if (!currentMode.value.config_server_url) return;
|
||||
configServerConnected.value = await isWebClientConnected();
|
||||
}, 1000)
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
const configServerConnectionStatus = computed(() => {
|
||||
if (currentMode.value.mode !== 'normal') {
|
||||
return 'unknown'
|
||||
}
|
||||
if (!currentMode.value.config_server_url) {
|
||||
return 'disconnected'
|
||||
}
|
||||
return configServerConnected.value ? 'connected' : 'connecting'
|
||||
})
|
||||
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="root" class="flex flex-col">
|
||||
<ConfigEditDialog v-model:visible="visible" :cur-network="curNetworkConfig" :readonly="activeStep !== '1'"
|
||||
:save-config="saveTomlConfig" :generate-config="parseNetworkConfig" />
|
||||
|
||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||
<About />
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="modeDialogVisible" modal :header="t('mode.switch_mode')" :style="{ width: '50vw' }">
|
||||
<ModeSwitcher v-model="editingMode" @uninstall-service="onUninstallService" @stop-service="onStopService" />
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="modeDialogVisible = false" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onModeSave" autofocus :loading="isModeSaving" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<div>
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<div class="flex items-center">
|
||||
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
|
||||
</div>
|
||||
</template>
|
||||
<Dialog v-model:visible="configServerDialogVisible" modal :header="t('config-server.title')"
|
||||
:style="{ width: '50vw' }">
|
||||
<div class="flex flex-col gap-3">
|
||||
<label for="config-server-address">{{ t('config-server.address') }}</label>
|
||||
<InputText id="config-server-address" v-model="(editingMode as WebClientConfig).config_server_url"
|
||||
:placeholder="t('config-server.address_placeholder')" />
|
||||
<small class="p-text-secondary whitespace-pre-wrap">{{ t('config-server.description') }}</small>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="configServerDialogVisible = false" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onConfigServerSave" autofocus
|
||||
:loading="isModeSaving" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<template #center>
|
||||
<div class="min-w-40">
|
||||
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||
:placeholder="t('select_network')" class="w-full">
|
||||
<template #value="slotProps">
|
||||
<div class="flex items-start content-center">
|
||||
<div class="mr-4 flex-col">
|
||||
<span>{{ slotProps.value.network_name }}</span>
|
||||
</div>
|
||||
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #option="slotProps">
|
||||
<div class="flex flex-col items-start content-center max-w-full">
|
||||
<div class="flex">
|
||||
<div class="mr-4">
|
||||
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
||||
</div>
|
||||
<Tag class="my-auto leading-3"
|
||||
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
|
||||
class="max-w-full overflow-hidden text-ellipsis">
|
||||
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
|
||||
? slotProps.option.peer_urls.join(', ')
|
||||
: slotProps.option.public_server_url }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)">
|
||||
{{
|
||||
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
</template>
|
||||
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
|
||||
|
||||
<template #end>
|
||||
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
||||
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
||||
</template>
|
||||
</Toolbar>
|
||||
<RemoteManagement v-if="clientRunning" class="flex-1 overflow-y-auto" :api="remoteClient"
|
||||
:pause-auto-refresh="isModeSaving" v-model:instance-id="instanceId" />
|
||||
<div v-else class="empty-state flex-1 flex flex-col items-center py-12">
|
||||
<i class="pi pi-server text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('client.not_running') }}
|
||||
</div>
|
||||
<Button @click="reconnectClient" :loading="isModeSaving" :label="t('client.retry')" icon="pi pi-replay"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
|
||||
<Panel class="h-full overflow-y-auto">
|
||||
<Stepper :value="activeStep">
|
||||
<StepList value="1">
|
||||
<Step value="1">
|
||||
{{ t('config_network') }}
|
||||
</Step>
|
||||
<Step value="2">
|
||||
{{ t('running') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
<StepPanels value="1">
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
||||
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||
:cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
|
||||
<div class="flex flex-col">
|
||||
<Status :cur-network-inst="curNetworkInst" />
|
||||
</div>
|
||||
<div class="flex pt-6 justify-center">
|
||||
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
</Panel>
|
||||
|
||||
<div>
|
||||
<Menubar :model="items" breakpoint="300px" />
|
||||
<InlineMessage v-if="messageBarSeverity !== Severity.None" class="absolute bottom-0 right-0" severity="error">
|
||||
{{ messageBarContent }}
|
||||
</InlineMessage>
|
||||
</div>
|
||||
<Menubar :model="setting_menu_items" breakpoint="795px">
|
||||
<template #item="{ item, props }">
|
||||
<a v-if="item.key === 'logging_menu'" v-bind="props.action" @click="toggle_log_menu">
|
||||
<span :class="item.icon" />
|
||||
<span class="p-menubar-item-label">{{ getLabel(item) }}</span>
|
||||
<span class="pi pi-angle-down p-menubar-item-icon text-[9px]"></span>
|
||||
</a>
|
||||
<a v-else v-bind="props.action">
|
||||
<span :class="item.icon" />
|
||||
<span class="p-menubar-item-label">{{ getLabel(item) }}</span>
|
||||
</a>
|
||||
</template>
|
||||
</Menubar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import { NetworkTypes } from 'easytier-frontend-lib'
|
||||
|
||||
export const useNetworkStore = defineStore('networkStore', {
|
||||
state: () => {
|
||||
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
||||
return {
|
||||
// for initially empty lists
|
||||
networkList: networkList as NetworkTypes.NetworkConfig[],
|
||||
// for data that is not yet loaded
|
||||
curNetwork: networkList[0],
|
||||
|
||||
// uuid -> instance
|
||||
instances: {} as Record<string, NetworkTypes.NetworkInstance>,
|
||||
|
||||
networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
|
||||
|
||||
autoStartInstIds: [] as string[],
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
lastNetwork(): NetworkTypes.NetworkConfig {
|
||||
return this.networkList[this.networkList.length - 1]
|
||||
},
|
||||
|
||||
curNetworkId(): string {
|
||||
return this.curNetwork.instance_id
|
||||
},
|
||||
|
||||
networkInstances(): Array<NetworkTypes.NetworkInstance> {
|
||||
return Object.values(this.instances)
|
||||
},
|
||||
|
||||
networkInstanceIds(): Array<string> {
|
||||
return Object.keys(this.instances)
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
addNewNetwork() {
|
||||
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
|
||||
},
|
||||
|
||||
delCurNetwork() {
|
||||
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
|
||||
this.networkList.splice(curNetworkIdx, 1)
|
||||
const nextCurNetworkIdx = Math.min(curNetworkIdx, this.networkList.length - 1)
|
||||
this.curNetwork = this.networkList[nextCurNetworkIdx]
|
||||
},
|
||||
|
||||
replaceCurNetwork(cfg: NetworkTypes.NetworkConfig) {
|
||||
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
|
||||
this.networkList[curNetworkIdx] = cfg
|
||||
this.curNetwork = cfg
|
||||
},
|
||||
|
||||
removeNetworkInstance(instanceId: string) {
|
||||
delete this.instances[instanceId]
|
||||
},
|
||||
|
||||
addNetworkInstance(instanceId: string) {
|
||||
this.instances[instanceId] = {
|
||||
instance_id: instanceId,
|
||||
running: false,
|
||||
error_msg: '',
|
||||
detail: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
clearNetworkInstances() {
|
||||
this.instances = {}
|
||||
},
|
||||
|
||||
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
|
||||
this.networkInfos = networkInfos
|
||||
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
||||
if (this.instances[instanceId] === undefined)
|
||||
this.addNetworkInstance(instanceId)
|
||||
|
||||
this.instances[instanceId].running = info.running
|
||||
this.instances[instanceId].error_msg = info.error_msg || ''
|
||||
this.instances[instanceId].detail = info
|
||||
}
|
||||
},
|
||||
|
||||
loadFromLocalStorage() {
|
||||
let networkList: NetworkTypes.NetworkConfig[]
|
||||
|
||||
// if localStorage default is [{}], instanceId will be undefined
|
||||
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
|
||||
networkList = networkList.map((cfg) => {
|
||||
return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.NetworkConfig
|
||||
})
|
||||
|
||||
// prevent a empty list from localStorage, should not happen
|
||||
if (networkList.length === 0)
|
||||
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
||||
|
||||
this.networkList = networkList
|
||||
this.curNetwork = this.networkList[0]
|
||||
|
||||
this.loadAutoStartInstIdsFromLocalStorage()
|
||||
},
|
||||
|
||||
saveToLocalStorage() {
|
||||
localStorage.setItem('networkList', JSON.stringify(this.networkList))
|
||||
},
|
||||
|
||||
saveAutoStartInstIdsToLocalStorage() {
|
||||
localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
|
||||
},
|
||||
|
||||
loadAutoStartInstIdsFromLocalStorage() {
|
||||
try {
|
||||
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
this.autoStartInstIds = []
|
||||
}
|
||||
},
|
||||
|
||||
addAutoStartInstId(instanceId: string) {
|
||||
if (!this.autoStartInstIds.includes(instanceId)) {
|
||||
this.autoStartInstIds.push(instanceId)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
|
||||
removeAutoStartInstId(instanceId: string) {
|
||||
const idx = this.autoStartInstIds.indexOf(instanceId)
|
||||
if (idx !== -1) {
|
||||
this.autoStartInstIds.splice(idx, 1)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
|
||||
isNoTunEnabled(instanceId: string): boolean {
|
||||
const cfg = this.networkList.find((cfg) => cfg.instance_id === instanceId)
|
||||
if (!cfg)
|
||||
return false
|
||||
return cfg.no_tun ?? false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot))
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.4.4"
|
||||
version = "2.5.0"
|
||||
edition = "2021"
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
@@ -64,6 +64,8 @@ uuid = { version = "1.5.0", features = [
|
||||
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
mimalloc = { version = "*" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
embed = ["dep:axum-embed"]
|
||||
|
||||
@@ -18,18 +18,17 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "4.3.3",
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"aura": "link:@primevue\\themes\\aura",
|
||||
"axios": "^1.7.7",
|
||||
"chart.js": "^4.5.0",
|
||||
"floating-vue": "^5.2",
|
||||
"ip-num": "1.5.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "4.3.3",
|
||||
"tailwindcss-primeui": "^0.3.4",
|
||||
"ts-md5": "^1.3.1",
|
||||
"uuid": "^11.0.2",
|
||||
"vue": "^3.5.12",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-i18n": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,5 +44,9 @@
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.12",
|
||||
"primevue": "^4.3.9"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password } from 'primevue'
|
||||
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
|
||||
import {
|
||||
addRow,
|
||||
DEFAULT_NETWORK_CONFIG,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
NetworkingMethod,
|
||||
removeRow
|
||||
} from '../types/network'
|
||||
import { defineProps, defineEmits, ref, } from 'vue'
|
||||
import { defineProps, defineEmits, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -157,6 +157,7 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||
{ field: 'disable_quic_input', help: 'disable_quic_input_help' },
|
||||
{ field: 'disable_p2p', help: 'disable_p2p_help' },
|
||||
{ field: 'p2p_only', help: 'p2p_only_help' },
|
||||
{ field: 'bind_device', help: 'bind_device_help' },
|
||||
{ field: 'no_tun', help: 'no_tun_help' },
|
||||
{ field: 'enable_exit_node', help: 'enable_exit_node_help' },
|
||||
@@ -164,21 +165,65 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'multi_thread', help: 'multi_thread_help' },
|
||||
{ field: 'proxy_forward_by_system', help: 'proxy_forward_by_system_help' },
|
||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||
]
|
||||
|
||||
const portForwardProtocolOptions = ref(["tcp","udp"]);
|
||||
const portForwardProtocolOptions = ref(["tcp", "udp"]);
|
||||
|
||||
const editingPortForward = ref(false);
|
||||
const editingPortForwardIndex = ref(-1);
|
||||
const editingPortForwardData = ref();
|
||||
|
||||
function openPortForwardEditor(index: number) {
|
||||
editingPortForwardIndex.value = index;
|
||||
// deep copy
|
||||
editingPortForwardData.value = JSON.parse(JSON.stringify(curNetwork.value.port_forwards[index]));
|
||||
editingPortForward.value = true;
|
||||
}
|
||||
|
||||
function addPortForward() {
|
||||
addRow(curNetwork.value.port_forwards)
|
||||
if (isCompact.value) {
|
||||
openPortForwardEditor(curNetwork.value.port_forwards.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function savePortForward() {
|
||||
curNetwork.value.port_forwards[editingPortForwardIndex.value] = editingPortForwardData.value;
|
||||
editingPortForward.value = false;
|
||||
}
|
||||
|
||||
const portForwardContainer = ref<HTMLElement | null>(null);
|
||||
const isCompact = ref(false);
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
if (portForwardContainer.value) {
|
||||
let resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
isCompact.value = entry.contentRect.width < 540;
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(portForwardContainer.value);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver && portForwardContainer.value) {
|
||||
resizeObserver.unobserve(portForwardContainer.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="frontend-lib">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-11/12 self-center ">
|
||||
<div class="w-full self-center ">
|
||||
<Panel :header="t('basic_settings')">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
@@ -227,9 +272,8 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
|
||||
class="grow" dropdown :complete-on-focus="false"
|
||||
@complete="searchPresetPublicServers" />
|
||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions" class="grow"
|
||||
dropdown :complete-on-focus="false" @complete="searchPresetPublicServers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,23 +352,6 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
|
||||
:format="false" :min="0" :max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<label for="">{{ t('rpc_portal_whitelists') }}</label>
|
||||
<AutoComplete id="rpc_portal_whitelists" v-model="curNetwork.rpc_portal_whitelists"
|
||||
:placeholder="t('chips_placeholder', ['127.0.0.0/8'])" class="w-full" multiple fluid
|
||||
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||
@@ -428,65 +455,87 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
|
||||
<Divider />
|
||||
|
||||
<Panel :header="t('port_forwards')" toggleable collapsed>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div ref="portForwardContainer" class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<div class="flex">
|
||||
<label for="port_forwards">{{ t('port_forwards_help') }}</label>
|
||||
</div>
|
||||
<div v-for="(row, index) in curNetwork.port_forwards" class="form-row">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
||||
<SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false"/>
|
||||
<div v-for="(row, index) in curNetwork.port_forwards" :key="index" class="form-row">
|
||||
<!-- Wide screen view -->
|
||||
<div v-if="!isCompact" class="flex gap-2 items-end">
|
||||
<SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false" />
|
||||
<div style="flex-grow: 4;">
|
||||
<InputGroup>
|
||||
<InputText
|
||||
v-model="row.bind_ip"
|
||||
:placeholder="t('port_forwards_bind_addr')"
|
||||
/>
|
||||
<InputText v-model="row.bind_ip" :placeholder="t('port_forwards_bind_addr')" />
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="row.bind_port" :format="false"
|
||||
inputId="horizontal-buttons" :step="1" mode="decimal" :min="1"
|
||||
:max="65535" fluid
|
||||
class="max-w-20"/>
|
||||
<InputNumber v-model="row.bind_port" :format="false" inputId="horizontal-buttons" :step="1"
|
||||
mode="decimal" :min="1" :max="65535" fluid class="max-w-20" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div style="flex-grow: 4;">
|
||||
<InputGroup>
|
||||
<InputText
|
||||
v-model="row.dst_ip"
|
||||
:placeholder="t('port_forwards_dst_addr')"
|
||||
/>
|
||||
<InputText v-model="row.dst_ip" :placeholder="t('port_forwards_dst_addr')" />
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="row.dst_port" :format="false"
|
||||
inputId="horizontal-buttons" :step="1" mode="decimal" :min="1"
|
||||
:max="65535" fluid
|
||||
class="max-w-20"/>
|
||||
<InputNumber v-model="row.dst_port" :format="false" inputId="horizontal-buttons" :step="1"
|
||||
mode="decimal" :min="1" :max="65535" fluid class="max-w-20" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div style="flex-grow: 1;">
|
||||
<Button
|
||||
v-if="curNetwork.port_forwards.length > 0"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
rounded
|
||||
@click="removeRow(index,curNetwork.port_forwards)"
|
||||
/>
|
||||
<Button v-if="curNetwork.port_forwards.length > 0" icon="pi pi-trash" severity="danger" text
|
||||
rounded @click="removeRow(index, curNetwork.port_forwards)" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Small screen view -->
|
||||
<div v-else class="flex justify-between items-center p-2 border-b">
|
||||
<span>{{ row.proto }}://{{ row.bind_ip }}:{{ row.bind_port }}/{{ row.dst_ip }}:{{
|
||||
row.dst_port }}</span>
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm" @click="openPortForwardEditor(index)" />
|
||||
<Button icon="pi pi-trash" class="p-button-sm p-button-danger"
|
||||
@click="removeRow(index, curNetwork.port_forwards)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-content-end mt-4">
|
||||
<Button
|
||||
icon="pi pi-plus"
|
||||
:label="t('port_forwards_add_btn')"
|
||||
severity="success"
|
||||
@click="addRow(curNetwork.port_forwards)"
|
||||
/>
|
||||
<Button icon="pi pi-plus" :label="t('port_forwards_add_btn')" severity="success"
|
||||
@click="addPortForward" />
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="editingPortForward" modal :header="t('edit_port_forward')"
|
||||
:style="{ width: '90vw', maxWidth: '600px' }">
|
||||
<div v-if="editingPortForwardData" class="flex flex-col gap-4">
|
||||
<SelectButton v-model="editingPortForwardData.proto" :options="portForwardProtocolOptions"
|
||||
:allow-empty="false" />
|
||||
<InputGroup>
|
||||
<InputText v-model="editingPortForwardData.bind_ip"
|
||||
:placeholder="t('port_forwards_bind_addr')" />
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="editingPortForwardData.bind_port" :format="false" :step="1" mode="decimal"
|
||||
:min="1" :max="65535" class="max-w-20" />
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputText v-model="editingPortForwardData.dst_ip" :placeholder="t('port_forwards_dst_addr')" />
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="editingPortForwardData.dst_port" :format="false" :step="1" mode="decimal"
|
||||
:min="1" :max="65535" class="max-w-20" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="editingPortForward = false"
|
||||
text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="savePortForward" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/20 dark:to-indigo-800/20 rounded-xl p-4 border border-blue-200 dark:border-blue-700 shadow-md hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center justify-center mb-3">
|
||||
<div class="flex gap-2 text-sm">
|
||||
<span class="flex items-center gap-1 w-32">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span class="text-green-600 dark:text-green-400 truncate">{{ t('upload') }}: {{ currentUpload }}/s</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1 w-32">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span class="text-blue-600 dark:text-blue-400 truncate">{{ t('download') }}: {{ currentDownload }}/s</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-32">
|
||||
<canvas ref="chartCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
LineController,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
} from 'chart.js'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 注册Chart.js组件
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
LineController,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
|
||||
interface Props {
|
||||
uploadRate: string
|
||||
downloadRate: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const chartCanvas = ref<HTMLCanvasElement>()
|
||||
let chart: ChartJS | null = null
|
||||
let updateTimer: number | null = null
|
||||
|
||||
// 存储历史数据,最多保存30个数据点(1分钟历史)
|
||||
const maxDataPoints = 120
|
||||
const uploadHistory: number[] = []
|
||||
const downloadHistory: number[] = []
|
||||
const timeLabels: string[] = []
|
||||
|
||||
const currentUpload = ref('0')
|
||||
const currentDownload = ref('0')
|
||||
|
||||
// 将带单位的速率字符串转换为字节数
|
||||
function parseRateToBytes(rateStr: string): number {
|
||||
if (!rateStr || rateStr === '0') return 0
|
||||
|
||||
const match = rateStr.match(/([0-9.]+)\s*([KMGT]?i?B)/i)
|
||||
if (!match) return 0
|
||||
|
||||
const value = parseFloat(match[1])
|
||||
const unit = match[2].toUpperCase()
|
||||
|
||||
const multipliers: { [key: string]: number } = {
|
||||
'B': 1,
|
||||
'KB': 1000,
|
||||
'KIB': 1024,
|
||||
'MB': 1000000,
|
||||
'MIB': 1024 * 1024,
|
||||
'GB': 1000000000,
|
||||
'GIB': 1024 * 1024 * 1024,
|
||||
'TB': 1000000000000,
|
||||
'TIB': 1024 * 1024 * 1024 * 1024
|
||||
}
|
||||
|
||||
return value * (multipliers[unit] || 1)
|
||||
}
|
||||
|
||||
// 格式化字节为可读格式
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1) return bytes.toFixed(1) + ' B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
function updateData() {
|
||||
const uploadBytes = parseRateToBytes(props.uploadRate)
|
||||
const downloadBytes = parseRateToBytes(props.downloadRate)
|
||||
|
||||
currentUpload.value = formatBytes(uploadBytes)
|
||||
currentDownload.value = formatBytes(downloadBytes)
|
||||
|
||||
// 添加新数据点
|
||||
uploadHistory.push(uploadBytes)
|
||||
downloadHistory.push(downloadBytes)
|
||||
|
||||
// 生成时间标签
|
||||
const now = new Date()
|
||||
const timeStr = now.toLocaleTimeString('zh-CN', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
timeLabels.push(timeStr)
|
||||
|
||||
// 保持数据点数量不超过最大值
|
||||
if (uploadHistory.length > maxDataPoints) {
|
||||
uploadHistory.shift()
|
||||
downloadHistory.shift()
|
||||
timeLabels.shift()
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
if (chart) {
|
||||
chart.data.labels = timeLabels
|
||||
chart.data.datasets[0].data = uploadHistory
|
||||
chart.data.datasets[1].data = downloadHistory
|
||||
chart.update('none')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
if (!chartCanvas.value) return
|
||||
|
||||
const ctx = chartCanvas.value.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
chart = new ChartJS(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('upload'),
|
||||
data: uploadHistory,
|
||||
borderColor: 'rgb(34, 197, 94)',
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4
|
||||
},
|
||||
{
|
||||
label: t('download'),
|
||||
data: downloadHistory,
|
||||
borderColor: 'rgb(59, 130, 246)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context: any) {
|
||||
const value = context.parsed.y
|
||||
return `${context.dataset.label}: ${formatBytes(value)}/s`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
font: {
|
||||
size: 8
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
beginAtZero: true,
|
||||
min: 0,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
callback: function (value: any) {
|
||||
return formatBytes(value as number)
|
||||
},
|
||||
font: {
|
||||
size: 8
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听props变化
|
||||
watch([() => props.uploadRate, () => props.downloadRate], () => {
|
||||
updateData()
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(async () => {
|
||||
// add initial point
|
||||
const now = new Date();
|
||||
for (let i = 0; i < maxDataPoints; i++) {
|
||||
let date = new Date(now.getTime() - (maxDataPoints - i) * 2000)
|
||||
const timeStr = date.toLocaleTimeString(navigator.language, {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
uploadHistory.push(0)
|
||||
downloadHistory.push(0)
|
||||
timeLabels.push(timeStr)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
initChart()
|
||||
updateData()
|
||||
|
||||
// 启动定时器,每2秒更新一次图表
|
||||
updateTimer = window.setInterval(() => {
|
||||
updateData()
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chart) {
|
||||
chart.destroy()
|
||||
}
|
||||
if (updateTimer) {
|
||||
clearInterval(updateTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,704 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Message, Select, Tag, useConfirm, useToast, type VirtualScrollerLazyEvent } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import * as Api from '../modules/api';
|
||||
import * as Utils from '../modules/utils';
|
||||
import * as NetworkTypes from '../types/network';
|
||||
import { type MenuItem } from 'primevue/menuitem';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
api: Api.RemoteClient;
|
||||
newConfigGenerator?: () => NetworkTypes.NetworkConfig;
|
||||
pauseAutoRefresh?: boolean;
|
||||
}>();
|
||||
|
||||
const instanceId = defineModel('instanceId', {
|
||||
type: String as () => string | undefined,
|
||||
required: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update']);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const configFile = ref();
|
||||
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const showConfigEditDialog = ref(false);
|
||||
const isEditingNetwork = ref(false); // Flag to indicate if we're in network editing mode
|
||||
const currentNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
|
||||
|
||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||
|
||||
const isRunning = (instanceId: string) => {
|
||||
return listInstanceIdResponse.value?.running_inst_ids.map(Utils.UuidToStr).includes(instanceId);
|
||||
}
|
||||
|
||||
const networkMetaCache = ref<Record<string, Api.NetworkMeta>>({});
|
||||
const loadNetworkMetas = async (instanceIds: string[]) => {
|
||||
const missingIds = instanceIds.filter(id => !networkMetaCache.value[id]);
|
||||
|
||||
if (missingIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const response = await props.api.get_network_metas(missingIds);
|
||||
Object.assign(networkMetaCache.value, response.metas);
|
||||
} catch (e) {
|
||||
console.error("Failed to load network metas", e);
|
||||
}
|
||||
};
|
||||
const onLazyLoadNetworkMetas = async (event: VirtualScrollerLazyEvent) => {
|
||||
const instanceIds = instanceList.value
|
||||
.slice(event.first, event.last + 1)
|
||||
.map(item => item.uuid);
|
||||
await loadNetworkMetas(instanceIds);
|
||||
};
|
||||
const currentNetworkMeta = computed(() => {
|
||||
if (!instanceId.value) {
|
||||
return undefined;
|
||||
}
|
||||
return networkMetaCache.value[instanceId.value];
|
||||
});
|
||||
const currentNetworkControl = {
|
||||
remoteSave: computed(() => {
|
||||
return Api.ConfigFilePermission.isRemoveSaveable(currentNetworkMeta.value?.config_permission ?? 0);
|
||||
}),
|
||||
editable: computed(() => {
|
||||
return Api.ConfigFilePermission.isEditable(currentNetworkMeta.value?.config_permission ?? 0);
|
||||
}),
|
||||
deletable: computed(() => {
|
||||
return Api.ConfigFilePermission.isDeletable(currentNetworkMeta.value?.config_permission ?? 0);
|
||||
})
|
||||
}
|
||||
|
||||
const instanceList = ref<Array<{ uuid: string; meta?: Api.NetworkMeta }>>([]);
|
||||
const updateInstanceList = () => {
|
||||
let insts = new Set<string>();
|
||||
let t = listInstanceIdResponse.value;
|
||||
if (t) {
|
||||
t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
|
||||
t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
|
||||
}
|
||||
|
||||
const newList = Array.from(insts).map((instance: string) => {
|
||||
return {
|
||||
uuid: instance,
|
||||
meta: networkMetaCache.value[instance]
|
||||
};
|
||||
});
|
||||
|
||||
if (JSON.stringify(newList) !== JSON.stringify(instanceList.value)) {
|
||||
instanceList.value = newList;
|
||||
}
|
||||
}
|
||||
watch(listInstanceIdResponse, updateInstanceList, { deep: false });
|
||||
watch(networkMetaCache, updateInstanceList, { deep: true });
|
||||
watch(instanceList, async (newVal) => {
|
||||
if (newVal) {
|
||||
const instanceIds = new Set(newVal.map(item => item.uuid));
|
||||
Object.keys(networkMetaCache.value).forEach(id => {
|
||||
if (!instanceIds.has(id)) {
|
||||
delete networkMetaCache.value[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const selectedInstanceId = computed({
|
||||
get() {
|
||||
return instanceList.value.find((instance) => instance.uuid === instanceId.value);
|
||||
},
|
||||
set(value: any) {
|
||||
console.log("set instanceId", value);
|
||||
instanceId.value = value ? value.uuid : undefined;
|
||||
}
|
||||
});
|
||||
watch(selectedInstanceId, async (newVal, oldVal) => {
|
||||
if (newVal?.uuid !== oldVal?.uuid && (networkIsDisabled.value || isEditingNetwork.value)) {
|
||||
await loadCurrentNetworkConfig();
|
||||
} else {
|
||||
await loadCurrentNetworkInfo();
|
||||
}
|
||||
|
||||
if (newVal?.uuid && !networkMetaCache.value[newVal.uuid]) {
|
||||
await loadNetworkMetas([newVal.uuid]);
|
||||
}
|
||||
});
|
||||
|
||||
const needShowNetworkStatus = computed(() => {
|
||||
if (!selectedInstanceId.value) {
|
||||
// nothing selected
|
||||
return false;
|
||||
}
|
||||
if (networkIsDisabled.value) {
|
||||
// network is disabled
|
||||
return false;
|
||||
}
|
||||
if (isEditingNetwork.value) {
|
||||
// editing network
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
const networkIsDisabled = computed(() => {
|
||||
if (!selectedInstanceId.value) {
|
||||
return false;
|
||||
}
|
||||
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
|
||||
});
|
||||
watch(networkIsDisabled, async (newVal, oldVal) => {
|
||||
if (newVal !== oldVal && newVal === true) {
|
||||
await loadCurrentNetworkConfig();
|
||||
}
|
||||
});
|
||||
|
||||
const loadCurrentNetworkConfig = async () => {
|
||||
currentNetworkConfig.value = undefined;
|
||||
|
||||
if (!selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api.get_network_config(selectedInstanceId.value!.uuid);
|
||||
currentNetworkConfig.value = ret;
|
||||
}
|
||||
|
||||
const stopNetwork = async () => {
|
||||
if (!selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, true);
|
||||
await loadNetworkInstanceIds();
|
||||
}
|
||||
|
||||
const confirm = useConfirm();
|
||||
const confirmDeleteNetwork = (event: any) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: 'Do you want to delete this network?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await props.api.delete_network(instanceId.value!);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
emits('update');
|
||||
},
|
||||
reject: () => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveAndRunNewNetwork = async () => {
|
||||
if (!currentNetworkConfig.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await props.api.delete_network(instanceId.value!);
|
||||
let ret = await props.api.run_network(currentNetworkConfig.value, currentNetworkControl.remoteSave.value);
|
||||
console.debug("saveAndRunNewNetwork", ret);
|
||||
|
||||
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
|
||||
await loadNetworkMetas([currentNetworkConfig.value.instance_id]);
|
||||
|
||||
selectedInstanceId.value = { uuid: currentNetworkConfig.value.instance_id };
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
emits('update');
|
||||
// showCreateNetworkDialog.value = false;
|
||||
isEditingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
}
|
||||
|
||||
const saveNetworkConfig = async () => {
|
||||
if (!currentNetworkConfig.value) {
|
||||
return;
|
||||
}
|
||||
await props.api.save_config(currentNetworkConfig.value);
|
||||
|
||||
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
|
||||
await loadNetworkMetas([currentNetworkConfig.value.instance_id]);
|
||||
|
||||
toast.add({ severity: 'success', summary: t("web.common.success"), detail: t("web.device_management.config_saved"), life: 2000 });
|
||||
}
|
||||
const newNetwork = async () => {
|
||||
const newNetworkConfig = props.newConfigGenerator?.() ?? NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
await props.api.save_config(newNetworkConfig);
|
||||
selectedInstanceId.value = { uuid: newNetworkConfig.instance_id };
|
||||
currentNetworkConfig.value = newNetworkConfig;
|
||||
await loadNetworkInstanceIds();
|
||||
}
|
||||
|
||||
const cancelEditNetwork = () => {
|
||||
isEditingNetwork.value = false;
|
||||
}
|
||||
|
||||
const editNetwork = async () => {
|
||||
if (!instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let ret = await props.api.get_network_config(instanceId.value!);
|
||||
console.debug("editNetwork", ret);
|
||||
currentNetworkConfig.value = ret;
|
||||
isEditingNetwork.value = true; // Switch to editing mode instead
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loadNetworkInstanceIds = async () => {
|
||||
listInstanceIdResponse.value = await props.api.list_network_instance_ids();
|
||||
}
|
||||
|
||||
const loadCurrentNetworkInfo = async () => {
|
||||
if (!selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
if (!needShowNetworkStatus.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let network_info = await props.api.get_network_info(selectedInstanceId.value.uuid);
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: selectedInstanceId.value.uuid,
|
||||
running: network_info?.running ?? false,
|
||||
error_msg: network_info?.error_msg ?? '',
|
||||
detail: network_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
|
||||
const exportConfig = async () => {
|
||||
if (!instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { instance_id, ...networkConfig } = await props.api.get_network_config(instanceId.value!);
|
||||
let { toml_config: tomlConfig, error } = await props.api.generate_config(networkConfig as NetworkTypes.NetworkConfig);
|
||||
if (error) {
|
||||
throw { response: { data: error } };
|
||||
}
|
||||
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const importConfig = () => {
|
||||
configFile.value.click();
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
const file = files ? files[0] : null;
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
let tomlConfig = e.target?.result?.toString();
|
||||
if (!tomlConfig) return;
|
||||
const resp = await props.api.parse_config(tomlConfig);
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
}
|
||||
|
||||
const config = resp.config;
|
||||
if (!config) return;
|
||||
|
||||
config.instance_id = currentNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
currentNetworkConfig.value = config;
|
||||
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
|
||||
}
|
||||
configFile.value.value = null;
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
const exportTomlFile = (context: string, name: string) => {
|
||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
|
||||
let link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', name);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
|
||||
let { toml_config: tomlConfig, error } = await props.api.generate_config(config);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return tomlConfig ?? '';
|
||||
}
|
||||
|
||||
const syncTomlConfig = async (tomlConfig: string): Promise<void> => {
|
||||
let resp = await props.api.parse_config(tomlConfig);
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
};
|
||||
const config = resp.config;
|
||||
if (!config) {
|
||||
throw new Error("Parsed config is empty");
|
||||
}
|
||||
config.instance_id = currentNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
currentNetworkConfig.value = config;
|
||||
}
|
||||
|
||||
// 响应式屏幕宽度
|
||||
const screenWidth = ref(window.innerWidth);
|
||||
const updateScreenWidth = () => {
|
||||
screenWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
// 菜单引用和菜单项
|
||||
const menuRef = ref();
|
||||
const actionMenu: Ref<MenuItem[]> = ref([
|
||||
{
|
||||
label: t('web.device_management.edit_network'),
|
||||
icon: 'pi pi-pencil',
|
||||
visible: () => !(networkIsDisabled.value ?? true) && currentNetworkControl.editable.value,
|
||||
command: () => editNetwork()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.export_config'),
|
||||
icon: 'pi pi-download',
|
||||
command: () => exportConfig()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.delete_network'),
|
||||
icon: 'pi pi-trash',
|
||||
class: 'p-error',
|
||||
visible: () => currentNetworkControl.deletable.value,
|
||||
command: () => confirmDeleteNetwork(new Event('click'))
|
||||
}
|
||||
]);
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
if (props.pauseAutoRefresh) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Promise.all([loadNetworkInstanceIds(), loadCurrentNetworkInfo()]);
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
|
||||
// 添加屏幕尺寸监听
|
||||
window.addEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
|
||||
// 移除屏幕尺寸监听
|
||||
window.removeEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="device-management">
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
|
||||
<!-- 网络选择和操作按钮始终在同一行 -->
|
||||
<div class="network-header bg-surface-50 p-3 rounded-lg shadow-sm mb-1">
|
||||
<div class="flex flex-row justify-between items-center gap-2" style="align-items: center;">
|
||||
<!-- 网络选择 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<IftaLabel class="w-full">
|
||||
<Select v-model="selectedInstanceId" :options="instanceList" optionLabel="uuid" class="w-full"
|
||||
inputId="dd-inst-id" :placeholder="t('web.device_management.select_network')"
|
||||
:pt="{ root: { class: 'network-select-container' } }" :virtualScrollerOptions="{
|
||||
lazy: true,
|
||||
onLazyLoad: onLazyLoadNetworkMetas,
|
||||
itemSize: 60,
|
||||
delay: 50
|
||||
}">
|
||||
<template #value="slotProps">
|
||||
<div v-if="slotProps.value" class="flex items-center content-center min-w-0">
|
||||
<div class="mr-4 flex-col min-w-0 flex-1">
|
||||
<span class="truncate block">
|
||||
|
||||
<span v-if="slotProps.value.meta">
|
||||
{{ slotProps.value.meta.network_name }} ({{ slotProps.value.uuid }})
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ slotProps.value.uuid }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<Tag class="my-auto leading-3 shrink-0"
|
||||
:severity="isRunning(slotProps.value.uuid) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.value.uuid) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
<span v-else>
|
||||
{{ slotProps.placeholder }}
|
||||
</span>
|
||||
</template>
|
||||
<template #option="slotProps">
|
||||
<div class="flex flex-col items-start content-center max-w-full">
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="mr-4 min-w-0 flex-1">
|
||||
<span class="truncate block">{{ t('network_name') }}: {{
|
||||
slotProps.option.meta.network_name }}</span>
|
||||
</div>
|
||||
<Tag class="my-auto leading-3 shrink-0"
|
||||
:severity="isRunning(slotProps.option.uuid) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.option.uuid) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
<div class="max-w-full overflow-hidden text-ellipsis text-gray-500">
|
||||
{{ slotProps.option.uuid }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
|
||||
t('web.device_management.network') }}</label>
|
||||
</IftaLabel>
|
||||
</div>
|
||||
|
||||
<!-- 简化的按钮区域 - 无论屏幕大小都显示 -->
|
||||
<div class="flex gap-2 shrink-0 button-container items-center">
|
||||
<!-- Create/Cancel button based on state -->
|
||||
<Button v-if="!isEditingNetwork" @click="newNetwork" icon="pi pi-plus"
|
||||
:label="screenWidth > 640 ? t('web.device_management.create_new') : undefined"
|
||||
:class="['create-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.create_network') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="primary" />
|
||||
|
||||
<Button v-else @click="cancelEditNetwork" icon="pi pi-times"
|
||||
:label="screenWidth > 640 ? t('web.device_management.cancel_edit') : undefined"
|
||||
:class="['cancel-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.cancel_edit') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="secondary" />
|
||||
|
||||
<!-- More actions menu -->
|
||||
<Menu ref="menuRef" :model="actionMenu" :popup="true" />
|
||||
<Button v-if="!isEditingNetwork && selectedInstanceId" icon="pi pi-ellipsis-v"
|
||||
class="p-button-rounded flex items-center justify-center" severity="help"
|
||||
style="width: 3rem !important; height: 3rem !important; font-size: 1.2rem"
|
||||
@click="menuRef.toggle($event)" :aria-label="t('web.device_management.more_actions')"
|
||||
:tooltip="t('web.device_management.more_actions')" tooltipOptions="{ position: 'bottom' }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="network-content bg-surface-0 p-4 rounded-lg shadow-sm">
|
||||
<!-- Network Creation Form -->
|
||||
<div v-if="isEditingNetwork || networkIsDisabled" class="network-creation-container">
|
||||
<div class="network-creation-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-plus-circle text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.edit_network') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 flex-wrap justify-start mb-3">
|
||||
<Button @click="showConfigEditDialog = true" icon="pi pi-file-edit"
|
||||
:label="t('web.device_management.edit_as_file')" iconPos="left" severity="secondary" />
|
||||
<Button @click="importConfig" icon="pi pi-upload" :label="t('web.device_management.import_config')"
|
||||
iconPos="left" severity="help" />
|
||||
<Button v-if="networkIsDisabled" @click="saveNetworkConfig" icon="pi pi-save"
|
||||
:label="t('web.device_management.save_config')" iconPos="left" severity="success" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Config :cur-network="currentNetworkConfig" @run-network="saveAndRunNewNetwork"></Config>
|
||||
</div>
|
||||
|
||||
<!-- Network Status (for running networks) -->
|
||||
<div v-else-if="needShowNetworkStatus" class="network-status-container">
|
||||
<div class="network-status-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-chart-line text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_status') }}</h2>
|
||||
</div>
|
||||
|
||||
<Status v-if="(curNetworkInfo?.error_msg ?? '') === ''" v-bind:cur-network-inst="curNetworkInfo"
|
||||
class="mb-4">
|
||||
</Status>
|
||||
<Message v-else severity="error" class="mb-4">{{ curNetworkInfo?.error_msg }}</Message>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<Button @click="stopNetwork" :disabled="!currentNetworkControl.deletable.value"
|
||||
:label="t('web.device_management.disable_network')" severity="danger" icon="pi pi-power-off"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state flex flex-col items-center py-12">
|
||||
<i class="pi pi-sitemap text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('web.device_management.no_network_selected') }}
|
||||
</div>
|
||||
<p class="text-secondary text-center mb-6 max-w-md">
|
||||
{{ t('web.device_management.select_existing_network_or_create_new') }}
|
||||
</p>
|
||||
<Button @click="newNetwork" :label="t('web.device_management.create_network')" icon="pi pi-plus"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keep only the config edit dialogs -->
|
||||
<!-- <ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
|
||||
:cur-network="currentNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" /> -->
|
||||
|
||||
<ConfigEditDialog v-model:visible="showConfigEditDialog" :cur-network="currentNetworkConfig"
|
||||
:generate-config="generateConfig" :save-config="syncTomlConfig" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.device-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.button-container {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
font-weight: 600;
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
/* 菜单样式定制 */
|
||||
:deep(.p-menu) {
|
||||
min-width: 12rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem) {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem-link) {
|
||||
padding: 0.65rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem-icon) {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem.p-error .p-menuitem-text,
|
||||
.p-menu .p-menuitem.p-error .p-menuitem-icon) {
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem:hover.p-error .p-menuitem-link) {
|
||||
background-color: var(--red-50);
|
||||
}
|
||||
|
||||
/* 按钮图标样式 */
|
||||
:deep(.p-button-icon-only) {
|
||||
width: 2.5rem !important;
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
:deep(.p-button-icon-only .p-button-icon) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 网络选择相关样式 */
|
||||
.network-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.network-select-container) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Dark mode adaptations */
|
||||
:deep(.bg-surface-50) {
|
||||
background-color: var(--surface-50, #f8fafc);
|
||||
}
|
||||
|
||||
:deep(.bg-surface-0) {
|
||||
background-color: var(--surface-card, #ffffff);
|
||||
}
|
||||
|
||||
:deep(.text-primary) {
|
||||
color: var(--primary-color, #3b82f6);
|
||||
}
|
||||
|
||||
:deep(.text-secondary) {
|
||||
color: var(--text-color-secondary, #64748b);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.bg-surface-50) {
|
||||
background-color: var(--surface-ground, #0f172a);
|
||||
}
|
||||
|
||||
:deep(.bg-surface-0) {
|
||||
background-color: var(--surface-card, #1e293b);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design for mobile devices */
|
||||
@media (max-width: 768px) {
|
||||
.network-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* 在小屏幕上缩短网络标签文本 */
|
||||
.network-label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,8 @@ import { NetworkInstance, type TunnelInfo, type NodeInfo, type PeerRoutePair } f
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
||||
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
||||
import { Badge, DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
||||
import NetworkChart from './NetworkChart.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
curNetworkInst: NetworkInstance | null,
|
||||
@@ -21,6 +22,7 @@ const peerRouteInfos = computed(() => {
|
||||
ipv4_addr: my_node_info?.virtual_ipv4,
|
||||
hostname: my_node_info?.hostname,
|
||||
version: my_node_info?.version,
|
||||
stun_info: my_node_info?.stun_info
|
||||
},
|
||||
}, ...(props.curNetworkInst.detail?.peer_route_pairs || [])]
|
||||
}
|
||||
@@ -144,6 +146,34 @@ interface Chip {
|
||||
icon: string
|
||||
}
|
||||
|
||||
// udp nat type
|
||||
enum NatType {
|
||||
// has NAT; but own a single public IP, port is not changed
|
||||
Unknown = 0,
|
||||
OpenInternet = 1,
|
||||
NoPAT = 2,
|
||||
FullCone = 3,
|
||||
Restricted = 4,
|
||||
PortRestricted = 5,
|
||||
Symmetric = 6,
|
||||
SymUdpFirewall = 7,
|
||||
SymmetricEasyInc = 8,
|
||||
SymmetricEasyDec = 9,
|
||||
};
|
||||
|
||||
const udpNatTypeStrMap = {
|
||||
[NatType.Unknown]: 'Unknown',
|
||||
[NatType.OpenInternet]: 'Open Internet',
|
||||
[NatType.NoPAT]: 'No PAT',
|
||||
[NatType.FullCone]: 'Full Cone',
|
||||
[NatType.Restricted]: 'Restricted',
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
const myNodeInfoChips = computed(() => {
|
||||
if (!props.curNetworkInst)
|
||||
return []
|
||||
@@ -212,35 +242,8 @@ const myNodeInfoChips = computed(() => {
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// udp nat type
|
||||
enum NatType {
|
||||
// has NAT; but own a single public IP, port is not changed
|
||||
Unknown = 0,
|
||||
OpenInternet = 1,
|
||||
NoPAT = 2,
|
||||
FullCone = 3,
|
||||
Restricted = 4,
|
||||
PortRestricted = 5,
|
||||
Symmetric = 6,
|
||||
SymUdpFirewall = 7,
|
||||
SymmetricEasyInc = 8,
|
||||
SymmetricEasyDec = 9,
|
||||
};
|
||||
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||
if (udpNatType !== undefined) {
|
||||
const udpNatTypeStrMap = {
|
||||
[NatType.Unknown]: 'Unknown',
|
||||
[NatType.OpenInternet]: 'Open Internet',
|
||||
[NatType.NoPAT]: 'No PAT',
|
||||
[NatType.FullCone]: 'Full Cone',
|
||||
[NatType.Restricted]: 'Restricted',
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
chips.push({
|
||||
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
|
||||
icon: '',
|
||||
@@ -271,6 +274,14 @@ function rxGlobalSum() {
|
||||
return globalSumCommon('stats.rx_bytes')
|
||||
}
|
||||
|
||||
function natType(info: PeerRoutePair): string {
|
||||
const udpNatType = info.route?.stun_info?.udp_nat_type;
|
||||
if (udpNatType !== undefined)
|
||||
return udpNatTypeStrMap[udpNatType as NatType]
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const peerCount = computed(() => {
|
||||
if (!peerRouteInfos.value)
|
||||
return 0
|
||||
@@ -285,6 +296,10 @@ let prevTxSum = 0
|
||||
let prevRxSum = 0
|
||||
const txRate = ref('0')
|
||||
const rxRate = ref('0')
|
||||
|
||||
// 控制节点详细信息chips的显示/隐藏
|
||||
const showNodeDetails = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
rateIntervalId = window.setInterval(() => {
|
||||
const curTxSum = txGlobalSum()
|
||||
@@ -365,36 +380,23 @@ function showEventLogs() {
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex w-full flex-col gap-y-5">
|
||||
<div class="m-0 flex flex-row justify-center gap-x-5">
|
||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green">
|
||||
<div class="font-bold">
|
||||
{{ t('peer_count') }}
|
||||
</div>
|
||||
<div class="text-5xl mt-1">
|
||||
{{ peerCount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
|
||||
<div class="font-bold">
|
||||
{{ t('upload') }}
|
||||
</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ txRate }}/s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
|
||||
<div class="font-bold">
|
||||
{{ t('download') }}
|
||||
</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ rxRate }}/s
|
||||
</div>
|
||||
<div class="gap-4">
|
||||
<!-- 网络流量图表 -->
|
||||
<div class="w-full">
|
||||
<NetworkChart :upload-rate="txRate" :download-rate="rxRate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||
<!-- 展开/收起节点详细信息的divider按钮 -->
|
||||
<div class="w-full">
|
||||
<Button @click="showNodeDetails = !showNodeDetails"
|
||||
:icon="showNodeDetails ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||
:label="showNodeDetails ? t('hide_node_details') : t('show_node_details')" severity="secondary" outlined
|
||||
class="w-full justify-center" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 节点详细信息chips,根据showNodeDetails状态显示/隐藏 -->
|
||||
<div v-show="showNodeDetails" class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||
class="mr-2 mt-2 text-sm" />
|
||||
</div>
|
||||
@@ -411,7 +413,15 @@ function showEventLogs() {
|
||||
|
||||
<Card>
|
||||
<template #title>
|
||||
{{ t('peer_info') }}
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ t('peer_info') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Badge :value="peerCount" severity="info"
|
||||
class="text-lg font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||
@@ -439,6 +449,7 @@ function showEventLogs() {
|
||||
<Column :field="txBytes" :header="t('upload_bytes')" />
|
||||
<Column :field="rxBytes" :header="t('download_bytes')" />
|
||||
<Column :field="lossRate" :header="t('loss_rate')" />
|
||||
<Column :field="natType" :header="t('nat_type')" />
|
||||
<Column :header="t('status.version')">
|
||||
<template #body="slotProps">
|
||||
<span>{{ version(slotProps.data) }}</span>
|
||||
@@ -455,4 +466,8 @@ function showEventLogs() {
|
||||
.p-timeline :deep(.p-timeline-event-opposite) {
|
||||
@apply flex-none;
|
||||
}
|
||||
|
||||
:deep(.p-datatable .p-datatable-column-title) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as Config } from './Config.vue';
|
||||
export { default as Status } from './Status.vue';
|
||||
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';
|
||||
export { default as RemoteManagement } from './RemoteManagement.vue';
|
||||
@@ -1,8 +1,8 @@
|
||||
import './style.css'
|
||||
|
||||
import type { App } from 'vue';
|
||||
import { Config, Status, ConfigEditDialog } from "./components";
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { Config, Status, ConfigEditDialog, RemoteManagement } from "./components";
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import PrimeVue from 'primevue/config'
|
||||
|
||||
import I18nUtils from './modules/i18n'
|
||||
@@ -44,8 +44,9 @@ export default {
|
||||
app.component('ConfigEditDialog', ConfigEditDialog);
|
||||
app.component('Status', Status);
|
||||
app.component('HumanEvent', HumanEvent);
|
||||
app.component('RemoteManagement', RemoteManagement);
|
||||
app.directive('tooltip', vTooltip as any);
|
||||
}
|
||||
};
|
||||
|
||||
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||
export { Config, ConfigEditDialog, RemoteManagement, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||
|
||||
@@ -48,6 +48,8 @@ hide_dock_icon: 隐藏 Dock 图标
|
||||
show_dock_icon: 显示 Dock 图标
|
||||
exit: 退出
|
||||
chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效
|
||||
show_node_details: 显示节点详细信息
|
||||
hide_node_details: 隐藏节点详细信息
|
||||
hostname_placeholder: '留空默认为主机名: {0}'
|
||||
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
||||
off_text: 点击关闭
|
||||
@@ -76,6 +78,7 @@ latency: 延迟
|
||||
upload_bytes: 上传
|
||||
download_bytes: 下载
|
||||
loss_rate: 丢包率
|
||||
nat_type: NAT 类型
|
||||
|
||||
flags_switch: 功能开关
|
||||
|
||||
@@ -103,6 +106,9 @@ disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的
|
||||
disable_p2p: 禁用 P2P
|
||||
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
|
||||
|
||||
p2p_only: 仅 P2P
|
||||
p2p_only_help: 仅与已经建立P2P连接的对等节点通信,不通过其他节点中转。
|
||||
|
||||
bind_device: 仅使用物理网卡
|
||||
bind_device_help: 仅使用物理网卡,避免 EasyTier 通过其他虚拟网建立连接。
|
||||
|
||||
@@ -126,6 +132,9 @@ proxy_forward_by_system_help: 通过系统内核转发子网代理数据包,
|
||||
disable_encryption: 禁用加密
|
||||
disable_encryption_help: 禁用对等节点通信的加密,默认为false,必须与对等节点相同
|
||||
|
||||
disable_tcp_hole_punching: 禁用TCP打洞
|
||||
disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
|
||||
disable_udp_hole_punching: 禁用UDP打洞
|
||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||
|
||||
@@ -161,6 +170,7 @@ port_forwards_help: "将本地端口转发到虚拟网络中的远程端口。
|
||||
port_forwards_bind_addr: "绑定地址,如:0.0.0.0"
|
||||
port_forwards_dst_addr: "目标地址,如:10.126.126.1"
|
||||
port_forwards_add_btn: "添加"
|
||||
edit_port_forward: "编辑端口转发"
|
||||
|
||||
mtu: MTU
|
||||
mtu_help: |
|
||||
@@ -218,6 +228,7 @@ event:
|
||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||
PortForwardAdded: 端口转发添加
|
||||
ProxyCidrsUpdated: 子网代理CIDR更新
|
||||
|
||||
web:
|
||||
login:
|
||||
@@ -286,9 +297,11 @@ web:
|
||||
network: 网络
|
||||
select_network: 选择网络
|
||||
create_network: 创建网络
|
||||
cancel_creation: 取消创建
|
||||
cancel_edit: 取消编辑
|
||||
more_actions: 更多操作
|
||||
edit_as_file: 编辑为文件
|
||||
save_config: 保存配置
|
||||
config_saved: 配置已保存
|
||||
import_config: 导入配置
|
||||
create_new: 创建新网络
|
||||
network_status: 网络状态
|
||||
@@ -330,4 +343,50 @@ web:
|
||||
confirm_password: 确认新密码
|
||||
language: 语言
|
||||
theme: 主题
|
||||
logout: 退出登录
|
||||
logout: 退出登录
|
||||
|
||||
mode:
|
||||
title: 模式
|
||||
switch_mode: 切换模式
|
||||
config_dir: 配置目录
|
||||
rpc_portal: RPC端口
|
||||
log_level: 日志级别
|
||||
log_dir: 日志目录
|
||||
remote_rpc_address: 远程RPC地址
|
||||
normal: 普通模式
|
||||
service: 服务模式
|
||||
remote: 远程模式
|
||||
normal_description: 直接运行EasyTier,适合本地使用
|
||||
service_description: 作为系统服务运行,支持开机自启和后台运行。退出GUI后服务仍在后台运行。
|
||||
remote_description: 连接到远程RPC服务,管理和控制远程节点
|
||||
service_status: 服务状态
|
||||
service_status_running: 运行中
|
||||
service_status_stopped: 已停止
|
||||
service_status_notinstalled: 未安装
|
||||
uninstall_service: 卸载服务
|
||||
stop_service: 停止服务
|
||||
uninstall_service_confirm: 确定要卸载服务吗?
|
||||
uninstall_service_success: 服务卸载成功,已切换回普通模式
|
||||
stop_service_success: 服务停止成功
|
||||
remote_rpc_address_empty: 远程RPC地址不能为空
|
||||
service_config_empty: 服务配置不能为空
|
||||
|
||||
config-server:
|
||||
title: 配置服务器
|
||||
address: 配置服务器地址
|
||||
address_placeholder: 例如:udp://127.0.0.1:22020/admin 或 admin
|
||||
description: |
|
||||
配置服务器地址,支持以下格式:
|
||||
完整URL:udp://127.0.0.1:22020/admin
|
||||
仅用户名:admin(使用官方服务器)
|
||||
留空:不连接配置服务器
|
||||
connection_status: 连接状态
|
||||
connected: ": 已连接"
|
||||
disconnected: ": 未连接"
|
||||
connecting: ": 连接中..."
|
||||
unknown: ""
|
||||
update_service_confirm: 将重启服务以应用更改,是否继续?
|
||||
|
||||
client:
|
||||
not_running: 无法连接至远程客户端
|
||||
retry: 重试
|
||||
|
||||
@@ -48,6 +48,8 @@ hide_dock_icon: Hide Dock Icon
|
||||
show_dock_icon: Show Dock Icon
|
||||
exit: Exit
|
||||
use_latency_first: Latency First Mode
|
||||
show_node_details: Show Node Details
|
||||
hide_node_details: Hide Node Details
|
||||
chips_placeholder: 'e.g: {0}, select from the dropdown after input'
|
||||
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
||||
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
||||
@@ -75,6 +77,7 @@ latency: Latency
|
||||
upload_bytes: Upload
|
||||
download_bytes: Download
|
||||
loss_rate: Loss Rate
|
||||
nat_type: NAT Type
|
||||
|
||||
flags_switch: Feature Switch
|
||||
|
||||
@@ -102,6 +105,9 @@ disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC pro
|
||||
disable_p2p: Disable P2P
|
||||
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
|
||||
|
||||
p2p_only: P2P Only
|
||||
p2p_only_help: Only communicate with peers that have already established P2P connections, do not relay through other nodes.
|
||||
|
||||
bind_device: Bind to Physical Device Only
|
||||
bind_device_help: Use only the physical network interface to prevent EasyTier from connecting via virtual networks.
|
||||
|
||||
@@ -125,6 +131,9 @@ proxy_forward_by_system_help: Forward packet to proxy networks via system kernel
|
||||
disable_encryption: Disable Encryption
|
||||
disable_encryption_help: Disable encryption for peers communication, default is false, must be same with peers
|
||||
|
||||
disable_tcp_hole_punching: Disable TCP Hole Punching
|
||||
disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
|
||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||
disable_udp_hole_punching_help: Disable udp hole punching
|
||||
|
||||
@@ -161,6 +170,7 @@ port_forwards_help: "forward local port to remote port in virtual network. e.g.:
|
||||
port_forwards_bind_addr: "Bind address, e.g.: 0.0.0.0"
|
||||
port_forwards_dst_addr: "Destination address, e.g.: 10.126.126.1"
|
||||
port_forwards_add_btn: "Add"
|
||||
edit_port_forward: "Edit Port Forward"
|
||||
|
||||
mtu: MTU
|
||||
mtu_help: |
|
||||
@@ -218,6 +228,7 @@ event:
|
||||
DhcpIpv4Changed: DhcpIpv4Changed
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||
PortForwardAdded: PortForwardAdded
|
||||
ProxyCidrsUpdated: ProxyCidrsUpdated
|
||||
|
||||
web:
|
||||
login:
|
||||
@@ -286,9 +297,11 @@ web:
|
||||
network: Network
|
||||
select_network: Select Network
|
||||
create_network: Create Network
|
||||
cancel_creation: Cancel Creation
|
||||
cancel_edit: Cancel Edit
|
||||
more_actions: More Actions
|
||||
edit_as_file: Edit as File
|
||||
save_config: Save Config
|
||||
config_saved: Config Saved
|
||||
import_config: Import Config
|
||||
create_new: Create New Network
|
||||
network_status: Network Status
|
||||
@@ -330,4 +343,50 @@ web:
|
||||
confirm_password: Confirm New Password
|
||||
language: Language
|
||||
theme: Theme
|
||||
logout: Logout
|
||||
logout: Logout
|
||||
|
||||
mode:
|
||||
title: Mode
|
||||
switch_mode: Switch Mode
|
||||
config_dir: Config Dir
|
||||
rpc_portal: RPC Portal
|
||||
log_level: Log Level
|
||||
log_dir: Log Dir
|
||||
remote_rpc_address: Remote RPC Address
|
||||
normal: Normal
|
||||
service: Service
|
||||
remote: Remote
|
||||
normal_description: Run EasyTier directly, suitable for local use.
|
||||
service_description: Run as a system service, supporting auto-startup and background operation. The service continues to run in the background after exiting the GUI.
|
||||
remote_description: Connect to a remote RPC service to manage and control remote nodes.
|
||||
service_status: Service Status
|
||||
service_status_running: Running
|
||||
service_status_stopped: Stopped
|
||||
service_status_notinstalled: Not Installed
|
||||
uninstall_service: Uninstall Service
|
||||
stop_service: Stop Service
|
||||
uninstall_service_confirm: Are you sure you want to uninstall the service?
|
||||
uninstall_service_success: Service uninstalled successfully, switched back to normal mode
|
||||
stop_service_success: Service stopped successfully
|
||||
remote_rpc_address_empty: Remote RPC Address cannot be empty
|
||||
service_config_empty: Service Config cannot be empty
|
||||
|
||||
config-server:
|
||||
title: Config Server
|
||||
address: Config Server Address
|
||||
address_placeholder: "e.g.: udp://127.0.0.1:22020/admin or admin"
|
||||
description: |
|
||||
Config server address, allowed formats:
|
||||
Full URL: udp://127.0.0.1:22020/admin
|
||||
Username only: admin (uses official server)
|
||||
Leave blank: Don't connect to a config server
|
||||
connection_status: Connection Status
|
||||
connected: ": Connected"
|
||||
disconnected: ": Disconnected"
|
||||
connecting: ": Connecting..."
|
||||
unknown: ""
|
||||
update_service_confirm: The service will be restarted to apply changes, do you want to continue?
|
||||
|
||||
client:
|
||||
not_running: Unable to connect to remote client.
|
||||
retry: Retry
|
||||
|
||||
@@ -1,241 +1,68 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { Md5 } from 'ts-md5'
|
||||
import { UUID } from './utils';
|
||||
import { NetworkConfig } from '../types/network';
|
||||
import { NetworkConfig, NetworkInstanceRunningInfo } from '../types/network';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 定义请求体数据结构
|
||||
export interface Credential {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
credentials: Credential;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
device_count: number;
|
||||
}
|
||||
|
||||
export interface ListNetworkInstanceIdResponse {
|
||||
running_inst_ids: Array<UUID>,
|
||||
disabled_inst_ids: Array<UUID>,
|
||||
}
|
||||
|
||||
export interface GenerateConfigRequest {
|
||||
config: NetworkConfig;
|
||||
}
|
||||
|
||||
export interface GenerateConfigResponse {
|
||||
toml_config?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigRequest {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigResponse {
|
||||
config?: NetworkConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private authFailedCb: Function | undefined;
|
||||
|
||||
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl + '/api/v1',
|
||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
this.authFailedCb = authFailedCb;
|
||||
|
||||
// 添加请求拦截器
|
||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config;
|
||||
}, (error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.debug('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
let response: AxiosResponse = error.response;
|
||||
if (response.status == 401 && this.authFailedCb) {
|
||||
console.error('Unauthorized:', response.data);
|
||||
this.authFailedCb();
|
||||
} else {
|
||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||
console.error('Response Error:', error.response.data);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已发出,但是没有收到响应
|
||||
console.error('Request Error:', error.request);
|
||||
} else {
|
||||
// 发生了一些问题导致请求未发出
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
try {
|
||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
console.log("register response:", response);
|
||||
return { success: true, message: 'Register success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
data.password = Md5.hashStr(data.password);
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
return { success: false, message: 'Invalid username or password', };
|
||||
} else {
|
||||
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
|
||||
}
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
await this.client.get('/auth/logout');
|
||||
if (this.authFailedCb) {
|
||||
this.authFailedCb();
|
||||
}
|
||||
}
|
||||
|
||||
public async change_password(new_password: string) {
|
||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
||||
}
|
||||
|
||||
public async check_login_status() {
|
||||
try {
|
||||
await this.client.get('/auth/check_login_status');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
const response = await this.client.get('/sessions');
|
||||
return response;
|
||||
}
|
||||
|
||||
public async list_machines(): Promise<Array<any>> {
|
||||
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
|
||||
return response.machines;
|
||||
}
|
||||
|
||||
public async list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> {
|
||||
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks');
|
||||
return response;
|
||||
}
|
||||
|
||||
public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> {
|
||||
await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, {
|
||||
disabled: disabled,
|
||||
});
|
||||
}
|
||||
|
||||
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
|
||||
return response.info.map;
|
||||
}
|
||||
|
||||
public async get_network_config(machine_id: string, inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/config/' + inst_id);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
||||
config: config,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
public async run_network(machine_id: string, config: any): Promise<undefined> {
|
||||
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
|
||||
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
||||
}
|
||||
|
||||
public async get_summary(): Promise<Summary> {
|
||||
const response = await this.client.get<any, Summary>('/summary');
|
||||
return response;
|
||||
}
|
||||
|
||||
public captcha_url() {
|
||||
return this.client.defaults.baseURL + '/auth/captcha';
|
||||
}
|
||||
|
||||
public async generate_config(config: GenerateConfigRequest): Promise<GenerateConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
|
||||
public async parse_config(config: ParseConfigRequest): Promise<ParseConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
export interface CollectNetworkInfoResponse {
|
||||
info: {
|
||||
map: Record<string, NetworkInstanceRunningInfo | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
export namespace ConfigFilePermission {
|
||||
export type Flags = number;
|
||||
export const READ_ONLY: Flags = 1 << 0;
|
||||
export const NO_DELETE: Flags = 1 << 1;
|
||||
export function hasPermission(perm: Flags, flag: Flags): boolean {
|
||||
return (perm & flag) === flag;
|
||||
}
|
||||
export function isRemoveSaveable(perm: Flags): boolean {
|
||||
return !hasPermission(perm, NO_DELETE);
|
||||
}
|
||||
export function isEditable(perm: Flags): boolean {
|
||||
return !hasPermission(perm, READ_ONLY);
|
||||
}
|
||||
export function isDeletable(perm: Flags): boolean {
|
||||
return !hasPermission(perm, NO_DELETE);
|
||||
}
|
||||
}
|
||||
|
||||
export interface NetworkMeta {
|
||||
network_name: string;
|
||||
config_permission: ConfigFilePermission.Flags;
|
||||
}
|
||||
|
||||
export interface GetNetworkMetasResponse {
|
||||
metas: Record<string, NetworkMeta>;
|
||||
}
|
||||
|
||||
export interface RemoteClient {
|
||||
validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>;
|
||||
run_network(config: NetworkConfig, save: boolean): Promise<undefined>;
|
||||
get_network_info(inst_id: string): Promise<NetworkInstanceRunningInfo | undefined>;
|
||||
list_network_instance_ids(): Promise<ListNetworkInstanceIdResponse>;
|
||||
delete_network(inst_id: string): Promise<undefined>;
|
||||
update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined>;
|
||||
save_config(config: NetworkConfig): Promise<undefined>;
|
||||
get_network_config(inst_id: string): Promise<NetworkConfig>;
|
||||
generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>;
|
||||
parse_config(toml_config: string): Promise<ParseConfigResponse>;
|
||||
get_network_metas(instance_ids: string[]): Promise<GetNetworkMetasResponse>;
|
||||
}
|
||||
@@ -31,7 +31,6 @@ export interface NetworkConfig {
|
||||
advanced_settings: boolean
|
||||
|
||||
listener_urls: string[]
|
||||
rpc_port: number
|
||||
latency_first: boolean
|
||||
|
||||
dev_name: string
|
||||
@@ -43,6 +42,7 @@ export interface NetworkConfig {
|
||||
enable_quic_proxy?: boolean
|
||||
disable_quic_input?: boolean
|
||||
disable_p2p?: boolean
|
||||
p2p_only?: boolean
|
||||
bind_device?: boolean
|
||||
no_tun?: boolean
|
||||
enable_exit_node?: boolean
|
||||
@@ -50,6 +50,7 @@ export interface NetworkConfig {
|
||||
multi_thread?: boolean
|
||||
proxy_forward_by_system?: boolean
|
||||
disable_encryption?: boolean
|
||||
disable_tcp_hole_punching?: boolean
|
||||
disable_udp_hole_punching?: boolean
|
||||
disable_sym_hole_punching?: boolean
|
||||
|
||||
@@ -70,8 +71,6 @@ export interface NetworkConfig {
|
||||
enable_magic_dns?: boolean
|
||||
enable_private_mode?: boolean
|
||||
|
||||
rpc_portal_whitelists: string[]
|
||||
|
||||
port_forwards: PortForwardConfig[]
|
||||
}
|
||||
|
||||
@@ -104,7 +103,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
'udp://0.0.0.0:11010',
|
||||
'wg://0.0.0.0:11011',
|
||||
],
|
||||
rpc_port: 0,
|
||||
latency_first: false,
|
||||
dev_name: '',
|
||||
|
||||
@@ -115,6 +113,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
enable_quic_proxy: false,
|
||||
disable_quic_input: false,
|
||||
disable_p2p: false,
|
||||
p2p_only: false,
|
||||
bind_device: true,
|
||||
no_tun: false,
|
||||
enable_exit_node: false,
|
||||
@@ -122,6 +121,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
multi_thread: true,
|
||||
proxy_forward_by_system: false,
|
||||
disable_encryption: false,
|
||||
disable_tcp_hole_punching: false,
|
||||
disable_udp_hole_punching: false,
|
||||
disable_sym_hole_punching: false,
|
||||
enable_relay_network_whitelist: false,
|
||||
@@ -135,7 +135,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
mapped_listeners: [],
|
||||
enable_magic_dns: false,
|
||||
enable_private_mode: false,
|
||||
rpc_portal_whitelists: [],
|
||||
port_forwards: [],
|
||||
}
|
||||
}
|
||||
@@ -314,4 +313,6 @@ export enum EventType {
|
||||
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
|
||||
|
||||
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
||||
|
||||
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ export default defineConfig({
|
||||
},
|
||||
// make sure to externalize deps that shouldn't be bundled
|
||||
// into your library
|
||||
external: ['vue'],
|
||||
external: ['vue', 'primevue'],
|
||||
output: {
|
||||
// Provide global variables to use in the UMD build
|
||||
// for externalized deps
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
primevue: 'primevue',
|
||||
},
|
||||
exports: "named"
|
||||
},
|
||||
|
||||
@@ -9,18 +9,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "4.3.3",
|
||||
"aura": "link:@primevue/themes/aura",
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"axios": "^1.7.7",
|
||||
"easytier-frontend-lib": "workspace:*",
|
||||
"primevue": "4.3.3",
|
||||
"primevue": "^4.3.9",
|
||||
"tailwindcss-primeui": "^0.3.4",
|
||||
"ts-md5": "^1.3.1",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "4",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0"
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@primevue/auto-import-resolver": "4.3.9",
|
||||
"@types/node": "^22.8.6",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { Card, Password, Button } from 'primevue';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const dialogRef = inject<any>('dialogRef');
|
||||
|
||||
const api = computed<Api.ApiClient>(() => dialogRef.value.data.api);
|
||||
const api = computed<ApiClient>(() => dialogRef.value.data.api);
|
||||
|
||||
const password = ref('');
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||
import {computed, ref} from 'vue';
|
||||
import { Api } from 'easytier-frontend-lib'
|
||||
import {AutoComplete, Divider, Button, Textarea} from "primevue";
|
||||
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||
import { computed, ref } from 'vue';
|
||||
import { AutoComplete, Divider, Button, Textarea } from "primevue";
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
|
||||
|
||||
const apiHost = ref<string>(getInitialApiHost())
|
||||
const apiHostSuggestions = ref<Array<string>>([])
|
||||
@@ -27,28 +27,24 @@ const errorMessage = ref<string>("");
|
||||
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
saveApiHost(apiHost.value)
|
||||
errorMessage.value = "";
|
||||
api.value?.generate_config({
|
||||
config: config
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
errorMessage.value = "Generation failed: " + res.error;
|
||||
} else if (res.toml_config) {
|
||||
toml_config.value = res.toml_config;
|
||||
} else {
|
||||
errorMessage.value = "Api server returned an unexpected response";
|
||||
}
|
||||
}).catch(err => {
|
||||
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
api.value?.get_remote_client("").generate_config(config).then((res) => {
|
||||
if (res.error) {
|
||||
errorMessage.value = "Generation failed: " + res.error;
|
||||
} else if (res.toml_config) {
|
||||
toml_config.value = res.toml_config;
|
||||
} else {
|
||||
errorMessage.value = "Api server returned an unexpected response";
|
||||
}
|
||||
}).catch(err => {
|
||||
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
};
|
||||
|
||||
const parseConfig = async () => {
|
||||
try {
|
||||
errorMessage.value = "";
|
||||
const res = await api.value?.parse_config({
|
||||
toml_config: toml_config.value
|
||||
});
|
||||
|
||||
const res = await api.value?.get_remote_client("").parse_config(toml_config.value);
|
||||
|
||||
if (res.error) {
|
||||
errorMessage.value = "Parse failed: " + res.error;
|
||||
} else if (res.config) {
|
||||
@@ -64,31 +60,29 @@ const parseConfig = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center m-5">
|
||||
<div class="sm:block md:flex w-full">
|
||||
<div class="sm:w-full md:w-1/2 p-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-11/12 self-center ">
|
||||
<label>ApiHost</label>
|
||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||
@complete="apiHostSearch" class="w-full" />
|
||||
<Divider />
|
||||
</div>
|
||||
</div>
|
||||
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||
</div>
|
||||
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
||||
<pre v-if="errorMessage" class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||
<Textarea
|
||||
v-model="toml_config"
|
||||
spellcheck="false"
|
||||
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
|
||||
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
|
||||
></Textarea>
|
||||
<div class="mt-3 flex justify-center">
|
||||
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center m-5">
|
||||
<div class="sm:block md:flex w-full">
|
||||
<div class="sm:w-full md:w-1/2 p-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-full self-center ">
|
||||
<label>ApiHost</label>
|
||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||
@complete="apiHostSearch" class="w-full" />
|
||||
<Divider />
|
||||
</div>
|
||||
</div>
|
||||
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||
</div>
|
||||
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
||||
<pre v-if="errorMessage"
|
||||
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||
<Textarea v-model="toml_config" spellcheck="false"
|
||||
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
|
||||
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"></Textarea>
|
||||
<div class="mt-3 flex justify-center">
|
||||
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Card, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
import { Utils } from 'easytier-frontend-lib';
|
||||
import ApiClient, { Summary } from '../modules/api';
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const summary = ref<Api.Summary | undefined>(undefined);
|
||||
const summary = ref<Summary | undefined>(undefined);
|
||||
|
||||
const loadSummary = async () => {
|
||||
const resp = await props.api?.get_summary();
|
||||
|
||||
@@ -3,9 +3,10 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Button, Drawer, ProgressSpinner, useToast, InputSwitch, Popover, Dropdown, Toolbar } from 'primevue';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
import { Utils } from 'easytier-frontend-lib';
|
||||
import DeviceDetails from './DeviceDetails.vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -15,7 +16,7 @@ declare const window: Window & typeof globalThis;
|
||||
const vTooltip = Tooltip;
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const detailPopover = ref();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { IftaLabel, Select, Button, ConfirmPopup, useConfirm, useToast, Divider, Menu } from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
|
||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { NetworkTypes, Utils, Api, RemoteManagement } from 'easytier-frontend-lib';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
api: Api.ApiClient;
|
||||
api: ApiClient;
|
||||
deviceList: Array<Utils.DeviceInfo> | undefined;
|
||||
}>();
|
||||
|
||||
@@ -16,7 +14,6 @@ const emits = defineEmits(['update']);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const deviceId = computed<string>(() => {
|
||||
return route.params.deviceId as string;
|
||||
@@ -30,469 +27,29 @@ const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
|
||||
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
||||
});
|
||||
|
||||
const configFile = ref();
|
||||
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const showConfigEditDialog = ref(false);
|
||||
const isCreatingNetwork = ref(false); // Flag to indicate if we're in network creation mode
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = new Set(deviceInfo.value?.running_network_instances || []);
|
||||
let t = listInstanceIdResponse.value;
|
||||
if (t) {
|
||||
t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
|
||||
t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
|
||||
}
|
||||
let options = Array.from(insts).map((instance: string) => {
|
||||
return { uuid: instance };
|
||||
});
|
||||
return options;
|
||||
});
|
||||
|
||||
const selectedInstanceId = computed({
|
||||
get() {
|
||||
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
|
||||
return instanceId.value;
|
||||
},
|
||||
set(value: any) {
|
||||
console.log("set instanceId", value);
|
||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
|
||||
set(value: string) {
|
||||
console.log("selectedInstanceId", value);
|
||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value } });
|
||||
}
|
||||
});
|
||||
|
||||
const needShowNetworkStatus = computed(() => {
|
||||
if (!selectedInstanceId.value) {
|
||||
// nothing selected
|
||||
return false;
|
||||
}
|
||||
if (networkIsDisabled.value) {
|
||||
// network is disabled
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
const remoteClient = computed<Api.RemoteClient>(() => props.api.get_remote_client(deviceId.value));
|
||||
|
||||
const networkIsDisabled = computed(() => {
|
||||
if (!selectedInstanceId.value) {
|
||||
return false;
|
||||
}
|
||||
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
|
||||
});
|
||||
|
||||
watch(selectedInstanceId, async (newVal, oldVal) => {
|
||||
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
|
||||
await loadDisabledNetworkConfig();
|
||||
}
|
||||
});
|
||||
|
||||
const disabledNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
|
||||
|
||||
const loadDisabledNetworkConfig = async () => {
|
||||
disabledNetworkConfig.value = undefined;
|
||||
|
||||
if (!deviceId.value || !selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api?.get_network_config(deviceId.value, selectedInstanceId.value.uuid);
|
||||
disabledNetworkConfig.value = ret;
|
||||
const newConfigGenerator = () => {
|
||||
const config = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
config.hostname = deviceInfo.value?.hostname;
|
||||
return config;
|
||||
}
|
||||
|
||||
const updateNetworkState = async (disabled: boolean) => {
|
||||
if (!deviceId.value || !selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disabled || !disabledNetworkConfig.value) {
|
||||
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
|
||||
} else if (disabledNetworkConfig.value) {
|
||||
await props.api?.delete_network(deviceId.value, disabledNetworkConfig.value.instance_id);
|
||||
await props.api?.run_network(deviceId.value, disabledNetworkConfig.value);
|
||||
}
|
||||
await loadNetworkInstanceIds();
|
||||
}
|
||||
|
||||
const confirm = useConfirm();
|
||||
const confirmDeleteNetwork = (event: any) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: 'Do you want to delete this network?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
emits('update');
|
||||
},
|
||||
reject: () => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
||||
// let ret = await props.api?.validate_config(deviceId.value, newNetworkConfig.value);
|
||||
// console.log("verifyNetworkConfig", ret);
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
const createNewNetwork = async () => {
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
||||
}
|
||||
let ret = await props.api?.run_network(deviceId.value, newNetworkConfig.value);
|
||||
console.debug("createNewNetwork", ret);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
emits('update');
|
||||
showCreateNetworkDialog.value = false;
|
||||
isCreatingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
}
|
||||
|
||||
const newNetwork = () => {
|
||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
newNetworkConfig.value.hostname = deviceInfo.value?.hostname;
|
||||
isEditing.value = false;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
}
|
||||
|
||||
const cancelNetworkCreation = () => {
|
||||
isCreatingNetwork.value = false;
|
||||
}
|
||||
|
||||
const editNetwork = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
isEditing.value = true;
|
||||
|
||||
try {
|
||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
console.debug("editNetwork", ret);
|
||||
newNetworkConfig.value = ret;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loadNetworkInstanceIds = async () => {
|
||||
if (!deviceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
listInstanceIdResponse.value = await props.api?.list_deivce_instance_ids(deviceId.value);
|
||||
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
|
||||
}
|
||||
|
||||
const loadDeviceInfo = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api?.get_network_info(deviceId.value, instanceId.value);
|
||||
let device_info = ret[instanceId.value];
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: instanceId.value,
|
||||
running: device_info.running,
|
||||
error_msg: device_info.error_msg,
|
||||
detail: device_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
|
||||
const exportConfig = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let networkConfig = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
delete networkConfig.instance_id;
|
||||
let { toml_config: tomlConfig, error } = await props.api?.generate_config({
|
||||
config: networkConfig
|
||||
});
|
||||
if (error) {
|
||||
throw { response: { data: error } };
|
||||
}
|
||||
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const importConfig = () => {
|
||||
configFile.value.click();
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
const file = files ? files[0] : null;
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
let tomlConfig = e.target?.result?.toString();
|
||||
if (!tomlConfig) return;
|
||||
const resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
}
|
||||
|
||||
const config = resp.config;
|
||||
if (!config) return;
|
||||
|
||||
config.instance_id = newNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
|
||||
Object.assign(newNetworkConfig.value, resp.config);
|
||||
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
|
||||
}
|
||||
configFile.value.value = null;
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
const exportTomlFile = (context: string, name: string) => {
|
||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
|
||||
let link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', name);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
|
||||
let { toml_config: tomlConfig, error } = await props.api?.generate_config({ config });
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return tomlConfig ?? '';
|
||||
}
|
||||
|
||||
const saveConfig = async (tomlConfig: string): Promise<void> => {
|
||||
let resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
};
|
||||
const config = resp.config;
|
||||
if (!config) {
|
||||
throw new Error("Parsed config is empty");
|
||||
}
|
||||
config.instance_id = disabledNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
if (networkIsDisabled.value) {
|
||||
disabledNetworkConfig.value = config;
|
||||
} else {
|
||||
newNetworkConfig.value = config;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式屏幕宽度
|
||||
const screenWidth = ref(window.innerWidth);
|
||||
const updateScreenWidth = () => {
|
||||
screenWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
// 菜单引用和菜单项
|
||||
const menuRef = ref();
|
||||
const actionMenu = ref([
|
||||
{
|
||||
label: t('web.device_management.edit_network'),
|
||||
icon: 'pi pi-pencil',
|
||||
command: () => editNetwork()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.export_config'),
|
||||
icon: 'pi pi-download',
|
||||
command: () => exportConfig()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.delete_network'),
|
||||
icon: 'pi pi-trash',
|
||||
class: 'p-error',
|
||||
command: () => confirmDeleteNetwork(new Event('click'))
|
||||
}
|
||||
]);
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
|
||||
// 添加屏幕尺寸监听
|
||||
window.addEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
|
||||
// 移除屏幕尺寸监听
|
||||
window.removeEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="device-management">
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
|
||||
<!-- 网络选择和操作按钮始终在同一行 -->
|
||||
<div class="network-header bg-surface-50 p-3 rounded-lg shadow-sm mb-1">
|
||||
<div class="flex flex-row justify-between items-center gap-2" style="align-items: center;">
|
||||
<!-- 网络选择 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<IftaLabel class="w-full">
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" class="w-full"
|
||||
inputId="dd-inst-id" :placeholder="t('web.device_management.select_network')"
|
||||
:pt="{ root: { class: 'network-select-container' } }" />
|
||||
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
|
||||
t('web.device_management.network') }}</label>
|
||||
</IftaLabel>
|
||||
</div>
|
||||
|
||||
<!-- 简化的按钮区域 - 无论屏幕大小都显示 -->
|
||||
<div class="flex gap-2 shrink-0 button-container items-center">
|
||||
<!-- Create/Cancel button based on state -->
|
||||
<Button v-if="!isCreatingNetwork" @click="newNetwork" icon="pi pi-plus"
|
||||
:label="screenWidth > 640 ? t('web.device_management.create_new') : undefined"
|
||||
:class="['create-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.create_network') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="primary" />
|
||||
|
||||
<Button v-else @click="cancelNetworkCreation" icon="pi pi-times"
|
||||
:label="screenWidth > 640 ? t('web.device_management.cancel_creation') : undefined"
|
||||
:class="['cancel-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.cancel_creation') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="secondary" />
|
||||
|
||||
<!-- More actions menu -->
|
||||
<Menu ref="menuRef" :model="actionMenu" :popup="true" />
|
||||
<Button v-if="!isCreatingNetwork && selectedInstanceId" icon="pi pi-ellipsis-v"
|
||||
class="p-button-rounded flex items-center justify-center" severity="help"
|
||||
style="width: 3rem !important; height: 3rem !important; font-size: 1.2rem"
|
||||
@click="menuRef.toggle($event)" :aria-label="t('web.device_management.more_actions')"
|
||||
:tooltip="t('web.device_management.more_actions')" tooltipOptions="{ position: 'bottom' }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="network-content bg-surface-0 p-4 rounded-lg shadow-sm">
|
||||
<!-- Network Creation Form -->
|
||||
<div v-if="isCreatingNetwork" class="network-creation-container">
|
||||
<div class="network-creation-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-plus-circle text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ isEditing ? t('web.device_management.edit_network') :
|
||||
t('web.device_management.create_network') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 flex-wrap justify-start mb-3">
|
||||
<Button @click="showConfigEditDialog = true" icon="pi pi-file-edit"
|
||||
:label="t('web.device_management.edit_as_file')" iconPos="left" severity="secondary" />
|
||||
<Button @click="importConfig" icon="pi pi-upload" :label="t('web.device_management.import_config')"
|
||||
iconPos="left" severity="help" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</div>
|
||||
|
||||
<!-- Network Status (for running networks) -->
|
||||
<div v-else-if="needShowNetworkStatus" class="network-status-container">
|
||||
<div class="network-status-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-chart-line text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_status') }}</h2>
|
||||
</div>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo" class="mb-4"></Status>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
|
||||
severity="warning" icon="pi pi-power-off" iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Configuration (for disabled networks) -->
|
||||
<div v-else-if="networkIsDisabled" class="network-config-container">
|
||||
<div class="network-config-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-cog text-secondary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_configuration') }}</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="disabledNetworkConfig" class="mb-4">
|
||||
<Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)" />
|
||||
</div>
|
||||
<div v-else class="network-loading-placeholder text-center py-8">
|
||||
<i class="pi pi-spin pi-spinner text-3xl text-primary mb-3"></i>
|
||||
<div class="text-xl text-secondary">{{ t('web.device_management.loading_network_configuration') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state flex flex-col items-center py-12">
|
||||
<i class="pi pi-sitemap text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('web.device_management.no_network_selected') }}
|
||||
</div>
|
||||
<p class="text-secondary text-center mb-6 max-w-md">
|
||||
{{ t('web.device_management.select_existing_network_or_create_new') }}
|
||||
</p>
|
||||
<Button @click="newNetwork" :label="t('web.device_management.create_network')" icon="pi pi-plus"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keep only the config edit dialogs -->
|
||||
<ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
|
||||
:cur-network="disabledNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" />
|
||||
|
||||
<ConfigEditDialog v-else v-model:visible="showConfigEditDialog" :cur-network="newNetworkConfig"
|
||||
:generate-config="generateConfig" :save-config="saveConfig" />
|
||||
</div>
|
||||
<RemoteManagement :api="remoteClient" v-model:instance-id="selectedInstanceId"
|
||||
:new-config-generator="newConfigGenerator" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,9 +3,10 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib';
|
||||
import { I18nUtils } from 'easytier-frontend-lib';
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -13,7 +14,7 @@ defineProps<{
|
||||
isRegistering: boolean;
|
||||
}>();
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
@@ -28,7 +29,7 @@ const captchaSrc = computed(() => api.value.captcha_url());
|
||||
const onSubmit = async () => {
|
||||
// Add your login logic here
|
||||
saveApiHost(apiHost.value);
|
||||
const credential: Api.Credential = { username: username.value, password: password.value, };
|
||||
const credential: Credential = { username: username.value, password: password.value, };
|
||||
let ret = await api.value?.login(credential);
|
||||
if (ret.success) {
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
@@ -43,8 +44,8 @@ const onSubmit = async () => {
|
||||
|
||||
const onRegister = async () => {
|
||||
saveApiHost(apiHost.value);
|
||||
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
let ret = await api.value?.register(registerReq);
|
||||
if (ret.success) {
|
||||
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
|
||||
@@ -108,12 +109,12 @@ onMounted(() => {
|
||||
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="register-username" class="block text-sm font-medium">{{ t('web.login.username')
|
||||
}}</label>
|
||||
}}</label>
|
||||
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="register-password" class="block text-sm font-medium">{{ t('web.login.password')
|
||||
}}</label>
|
||||
}}</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
:feedback="false" class="w-full" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib'
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -7,13 +7,14 @@ import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const api = computed<Api.ApiClient | undefined>(() => {
|
||||
const api = computed<ApiClient | undefined>(() => {
|
||||
try {
|
||||
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
|
||||
return new ApiClient(atob(route.params.apiHost as string), () => {
|
||||
router.push({ name: 'login' });
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import './style.css'
|
||||
import App from './App.vue'
|
||||
import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { type Api, type NetworkTypes, Utils } from 'easytier-frontend-lib';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 定义请求体数据结构
|
||||
export interface Credential {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
credentials: Credential;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
device_count: number;
|
||||
}
|
||||
|
||||
export interface ListNetworkInstanceIdResponse {
|
||||
running_inst_ids: Array<Utils.UUID>,
|
||||
disabled_inst_ids: Array<Utils.UUID>,
|
||||
}
|
||||
|
||||
export interface GenerateConfigRequest {
|
||||
config: NetworkTypes.NetworkConfig;
|
||||
}
|
||||
|
||||
export interface GenerateConfigResponse {
|
||||
toml_config?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigRequest {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigResponse {
|
||||
config?: NetworkTypes.NetworkConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private authFailedCb: Function | undefined;
|
||||
|
||||
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl.replace(/\/+$/, '') + '/api/v1',
|
||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
this.authFailedCb = authFailedCb;
|
||||
|
||||
// 添加请求拦截器
|
||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config;
|
||||
}, (error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.debug('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
let response: AxiosResponse = error.response;
|
||||
if (response.status == 401 && this.authFailedCb) {
|
||||
console.error('Unauthorized:', response.data);
|
||||
this.authFailedCb();
|
||||
} else {
|
||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||
console.error('Response Error:', error.response.data);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已发出,但是没有收到响应
|
||||
console.error('Request Error:', error.request);
|
||||
} else {
|
||||
// 发生了一些问题导致请求未发出
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
try {
|
||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
console.log("register response:", response);
|
||||
return { success: true, message: 'Register success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
data.password = Md5.hashStr(data.password);
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
return { success: false, message: 'Invalid username or password', };
|
||||
} else {
|
||||
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
|
||||
}
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
await this.client.get('/auth/logout');
|
||||
if (this.authFailedCb) {
|
||||
this.authFailedCb();
|
||||
}
|
||||
}
|
||||
|
||||
public async change_password(new_password: string) {
|
||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
||||
}
|
||||
|
||||
public async check_login_status() {
|
||||
try {
|
||||
await this.client.get('/auth/check_login_status');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
const response = await this.client.get('/sessions');
|
||||
return response;
|
||||
}
|
||||
|
||||
public async list_machines(): Promise<Array<any>> {
|
||||
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
|
||||
return response.machines;
|
||||
}
|
||||
|
||||
public async get_summary(): Promise<Summary> {
|
||||
const response = await this.client.get<any, Summary>('/summary');
|
||||
return response;
|
||||
}
|
||||
|
||||
public captcha_url() {
|
||||
return this.client.defaults.baseURL + '/auth/captcha';
|
||||
}
|
||||
|
||||
public get_remote_client(machine_id: string): Api.RemoteClient {
|
||||
return new WebRemoteClient(machine_id, this.client);
|
||||
}
|
||||
}
|
||||
|
||||
class WebRemoteClient implements Api.RemoteClient {
|
||||
private machine_id: string;
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(machine_id: string, client: AxiosInstance) {
|
||||
this.machine_id = machine_id;
|
||||
this.client = client;
|
||||
}
|
||||
async validate_config(config: NetworkTypes.NetworkConfig): Promise<Api.ValidateConfigResponse> {
|
||||
const response = await this.client.post<NetworkTypes.NetworkConfig, ValidateConfigResponse>(`/machines/${this.machine_id}/validate-config`, {
|
||||
config: config,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
|
||||
await this.client.post<string>(`/machines/${this.machine_id}/networks`, {
|
||||
config: config,
|
||||
save: save
|
||||
});
|
||||
}
|
||||
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {
|
||||
const response = await this.client.get<any, Api.CollectNetworkInfoResponse>('/machines/' + this.machine_id + '/networks/info/' + inst_id);
|
||||
return response.info.map[inst_id];
|
||||
}
|
||||
async list_network_instance_ids(): Promise<Api.ListNetworkInstanceIdResponse> {
|
||||
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + this.machine_id + '/networks');
|
||||
return response;
|
||||
}
|
||||
async delete_network(inst_id: string): Promise<undefined> {
|
||||
await this.client.delete<string>(`/machines/${this.machine_id}/networks/${inst_id}`);
|
||||
}
|
||||
async update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined> {
|
||||
await this.client.put<string>('/machines/' + this.machine_id + '/networks/' + inst_id, {
|
||||
disabled: disabled,
|
||||
});
|
||||
}
|
||||
async save_config(config: NetworkTypes.NetworkConfig): Promise<undefined> {
|
||||
await this.client.put(`/machines/${this.machine_id}/networks/config/${config.instance_id}`, { config });
|
||||
}
|
||||
async get_network_config(inst_id: string): Promise<NetworkTypes.NetworkConfig> {
|
||||
const response = await this.client.get<any, NetworkTypes.NetworkConfig>('/machines/' + this.machine_id + '/networks/config/' + inst_id);
|
||||
return response;
|
||||
}
|
||||
async generate_config(config: NetworkTypes.NetworkConfig): Promise<Api.GenerateConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', { config });
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
async parse_config(toml_config: string): Promise<Api.ParseConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', { toml_config });
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
async get_network_metas(instance_ids: string[]): Promise<Api.GetNetworkMetasResponse> {
|
||||
const response = await this.client.post<any, Api.GetNetworkMetasResponse>(`/machines/${this.machine_id}/networks/metas`, {
|
||||
instance_ids: instance_ids
|
||||
});
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
@@ -7,13 +7,19 @@ use std::sync::{
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::{proto::web::HeartbeatRequest, tunnel::TunnelListener};
|
||||
use easytier::{
|
||||
proto::{
|
||||
api::manage::WebClientService, rpc_types::controller::BaseController, web::HeartbeatRequest,
|
||||
},
|
||||
rpc_service::remote_client::{self, RemoteClientManager},
|
||||
tunnel::TunnelListener,
|
||||
};
|
||||
use maxminddb::geoip2;
|
||||
use session::{Location, Session};
|
||||
use storage::{Storage, StorageToken};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::db::{Db, UserIdInDb};
|
||||
use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
|
||||
|
||||
#[derive(rust_embed::Embed)]
|
||||
#[folder = "resources/"]
|
||||
@@ -152,7 +158,7 @@ impl ClientManager {
|
||||
s.data().read().await.location().cloned()
|
||||
}
|
||||
|
||||
pub fn db(&self) -> &Db {
|
||||
fn db(&self) -> &Db {
|
||||
self.storage.db()
|
||||
}
|
||||
|
||||
@@ -245,11 +251,38 @@ impl ClientManager {
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
RemoteClientManager<
|
||||
(UserIdInDb, uuid::Uuid),
|
||||
user_running_network_configs::Model,
|
||||
sea_orm::DbErr,
|
||||
> for ClientManager
|
||||
{
|
||||
fn get_rpc_client(
|
||||
&self,
|
||||
(user_id, machine_id): (UserIdInDb, uuid::Uuid),
|
||||
) -> Option<Box<dyn WebClientService<Controller = BaseController> + Send>> {
|
||||
let s = self.get_session_by_machine_id(user_id, &machine_id)?;
|
||||
Some(s.scoped_rpc_client())
|
||||
}
|
||||
|
||||
fn get_storage(
|
||||
&self,
|
||||
) -> &impl remote_client::Storage<
|
||||
(UserIdInDb, uuid::Uuid),
|
||||
user_running_network_configs::Model,
|
||||
sea_orm::DbErr,
|
||||
> {
|
||||
self.storage.db()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use easytier::{
|
||||
instance_manager::NetworkInstanceManager,
|
||||
tunnel::{
|
||||
common::tests::wait_for_condition,
|
||||
udp::{UdpTunnelConnector, UdpTunnelListener},
|
||||
@@ -273,7 +306,13 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
|
||||
let _c = WebClient::new(connector, "test", "test");
|
||||
let _c = WebClient::new(
|
||||
connector,
|
||||
"test",
|
||||
"test",
|
||||
Arc::new(NetworkInstanceManager::new()),
|
||||
None,
|
||||
);
|
||||
|
||||
wait_for_condition(
|
||||
|| async { mgr.client_sessions.len() == 1 },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user