mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
Compare commits
8 Commits
36624aff49
...
830c0773dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830c0773dc | ||
|
|
5da9f99698 | ||
|
|
decdeffcf6 | ||
|
|
7b7dc79c74 | ||
|
|
fa4557337b | ||
|
|
d6d15652ca | ||
|
|
a73fafaf9f | ||
|
|
6f4ddb6db3 |
415
.github/workflows/pr-ai-slop-review.lock.yml
generated
vendored
415
.github/workflows/pr-ai-slop-review.lock.yml
generated
vendored
@ -12,7 +12,7 @@
|
|||||||
# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
|
# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
|
||||||
# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
|
# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
|
||||||
#
|
#
|
||||||
# This file was automatically generated by gh-aw (v0.64.0). DO NOT EDIT.
|
# This file was automatically generated by gh-aw (v0.62.5). DO NOT EDIT.
|
||||||
#
|
#
|
||||||
# To update this file, edit the corresponding .md file and run:
|
# To update this file, edit the corresponding .md file and run:
|
||||||
# gh aw compile
|
# gh aw compile
|
||||||
@ -24,22 +24,17 @@
|
|||||||
# signs of one-shot AI-generated changes, then posts a maintainer-focused
|
# signs of one-shot AI-generated changes, then posts a maintainer-focused
|
||||||
# comment when the risk is high enough to warrant follow-up.
|
# comment when the risk is high enough to warrant follow-up.
|
||||||
#
|
#
|
||||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6b65c1d60eaf3a611755c7def95a13c3673df2f25ff609508e99d59c77d57d8d","compiler_version":"v0.64.0","strict":true,"agent_id":"copilot"}
|
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"90464e2f3880b176a101872f067e5873c3c3f92ac7ab6821861dad3d11f835df","compiler_version":"v0.62.5","strict":true,"agent_id":"copilot"}
|
||||||
|
|
||||||
name: "PR AI Slop Review"
|
name: "PR AI Slop Review"
|
||||||
"on":
|
"on":
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
- reopened
|
||||||
- synchronize
|
- synchronize
|
||||||
# roles: all # Roles processed as role check in pre-activation job
|
# roles: all # Roles processed as role check in pre-activation job
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
aw_context:
|
|
||||||
default: ""
|
|
||||||
description: Agent caller context (used internally by Agentic Workflows).
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@ -65,7 +60,7 @@ jobs:
|
|||||||
title: ${{ steps.sanitized.outputs.title }}
|
title: ${{ steps.sanitized.outputs.title }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@63aa903fe409698e15e5718ad89366a72bfe6a89 # v0.65.5
|
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Generate agentic run info
|
- name: Generate agentic run info
|
||||||
@ -73,17 +68,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_AW_INFO_ENGINE_ID: "copilot"
|
GH_AW_INFO_ENGINE_ID: "copilot"
|
||||||
GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
|
GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
|
||||||
GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }}
|
GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }}
|
||||||
GH_AW_INFO_VERSION: "latest"
|
GH_AW_INFO_VERSION: ""
|
||||||
GH_AW_INFO_AGENT_VERSION: "latest"
|
GH_AW_INFO_AGENT_VERSION: "latest"
|
||||||
GH_AW_INFO_CLI_VERSION: "v0.64.0"
|
GH_AW_INFO_CLI_VERSION: "v0.62.5"
|
||||||
GH_AW_INFO_WORKFLOW_NAME: "PR AI Slop Review"
|
GH_AW_INFO_WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
GH_AW_INFO_EXPERIMENTAL: "false"
|
GH_AW_INFO_EXPERIMENTAL: "false"
|
||||||
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
|
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
|
||||||
GH_AW_INFO_STAGED: "false"
|
GH_AW_INFO_STAGED: "false"
|
||||||
GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
|
GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
|
||||||
GH_AW_INFO_FIREWALL_ENABLED: "true"
|
GH_AW_INFO_FIREWALL_ENABLED: "true"
|
||||||
GH_AW_INFO_AWF_VERSION: "v0.25.0"
|
GH_AW_INFO_AWF_VERSION: "v0.24.5"
|
||||||
GH_AW_INFO_AWMG_VERSION: ""
|
GH_AW_INFO_AWMG_VERSION: ""
|
||||||
GH_AW_INFO_FIREWALL_TYPE: "squid"
|
GH_AW_INFO_FIREWALL_TYPE: "squid"
|
||||||
GH_AW_COMPILED_STRICT: "true"
|
GH_AW_COMPILED_STRICT: "true"
|
||||||
@ -130,7 +125,7 @@ jobs:
|
|||||||
- name: Create prompt with built-in context
|
- name: Create prompt with built-in context
|
||||||
env:
|
env:
|
||||||
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
||||||
GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
|
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
|
||||||
GH_AW_GITHUB_ACTOR: ${{ github.actor }}
|
GH_AW_GITHUB_ACTOR: ${{ github.actor }}
|
||||||
GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
|
GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
|
||||||
GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
|
GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
|
||||||
@ -139,7 +134,6 @@ jobs:
|
|||||||
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
|
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
|
GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
|
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
|
||||||
# poutine:ignore untrusted_checkout_exec
|
|
||||||
run: |
|
run: |
|
||||||
bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh
|
bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh
|
||||||
{
|
{
|
||||||
@ -152,7 +146,7 @@ jobs:
|
|||||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
||||||
cat << 'GH_AW_PROMPT_EOF'
|
cat << 'GH_AW_PROMPT_EOF'
|
||||||
<safe-output-tools>
|
<safe-output-tools>
|
||||||
Tools: add_comment, add_labels, remove_labels(max:2), missing_tool, missing_data, noop
|
Tools: add_comment, add_labels, remove_labels, missing_tool, missing_data, noop
|
||||||
</safe-output-tools>
|
</safe-output-tools>
|
||||||
<github-context>
|
<github-context>
|
||||||
The following GitHub context information is available for this workflow:
|
The following GitHub context information is available for this workflow:
|
||||||
@ -237,16 +231,14 @@ jobs:
|
|||||||
- name: Validate prompt placeholders
|
- name: Validate prompt placeholders
|
||||||
env:
|
env:
|
||||||
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
||||||
# poutine:ignore untrusted_checkout_exec
|
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh
|
run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh
|
||||||
- name: Print prompt
|
- name: Print prompt
|
||||||
env:
|
env:
|
||||||
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
||||||
# poutine:ignore untrusted_checkout_exec
|
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh
|
run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh
|
||||||
- name: Upload activation artifact
|
- name: Upload activation artifact
|
||||||
if: success()
|
if: success()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: activation
|
name: activation
|
||||||
path: |
|
path: |
|
||||||
@ -270,6 +262,8 @@ jobs:
|
|||||||
GH_AW_WORKFLOW_ID_SANITIZED: praislopreview
|
GH_AW_WORKFLOW_ID_SANITIZED: praislopreview
|
||||||
outputs:
|
outputs:
|
||||||
checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
|
checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
|
||||||
|
detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
|
||||||
|
detection_success: ${{ steps.detection_conclusion.outputs.success }}
|
||||||
has_patch: ${{ steps.collect_output.outputs.has_patch }}
|
has_patch: ${{ steps.collect_output.outputs.has_patch }}
|
||||||
inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }}
|
inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }}
|
||||||
model: ${{ needs.activation.outputs.model }}
|
model: ${{ needs.activation.outputs.model }}
|
||||||
@ -277,15 +271,14 @@ jobs:
|
|||||||
output_types: ${{ steps.collect_output.outputs.output_types }}
|
output_types: ${{ steps.collect_output.outputs.output_types }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@63aa903fe409698e15e5718ad89366a72bfe6a89 # v0.65.5
|
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Set runtime paths
|
- name: Set runtime paths
|
||||||
id: set-runtime-paths
|
|
||||||
run: |
|
run: |
|
||||||
echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT"
|
echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_ENV"
|
||||||
echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT"
|
echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_ENV"
|
||||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT"
|
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_ENV"
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
@ -327,22 +320,16 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_HOST: github.com
|
GH_HOST: github.com
|
||||||
- name: Install AWF binary
|
- name: Install AWF binary
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.0
|
run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.24.5
|
||||||
- name: Parse integrity filter lists
|
|
||||||
id: parse-guard-vars
|
|
||||||
env:
|
|
||||||
GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }}
|
|
||||||
GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }}
|
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh
|
|
||||||
- name: Download container images
|
- name: Download container images
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.0 ghcr.io/github/gh-aw-firewall/squid:0.25.0 ghcr.io/github/gh-aw-mcpg:v0.2.6 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine
|
run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.5 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.5 ghcr.io/github/gh-aw-firewall/squid:0.24.5 ghcr.io/github/gh-aw-mcpg:v0.1.20 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine
|
||||||
- name: Write Safe Outputs Config
|
- name: Write Safe Outputs Config
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs
|
mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs
|
||||||
mkdir -p /tmp/gh-aw/safeoutputs
|
mkdir -p /tmp/gh-aw/safeoutputs
|
||||||
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
||||||
cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
|
cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
|
||||||
{"add_comment":{"hide_older_comments":true,"max":1},"add_labels":{"allowed":["ai-slop:high","ai-slop:med"],"max":1},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["ai-slop:high","ai-slop:med"],"max":2}}
|
{"add_comment":{"max":1},"add_labels":{"allowed":["ai-slop:high","ai-slop:med"],"max":1},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["ai-slop:high","ai-slop:med"],"max":2}}
|
||||||
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
|
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
|
||||||
- name: Write Safe Outputs Tools
|
- name: Write Safe Outputs Tools
|
||||||
run: |
|
run: |
|
||||||
@ -516,7 +503,7 @@ jobs:
|
|||||||
- name: Start MCP Gateway
|
- name: Start MCP Gateway
|
||||||
id: start-mcp-gateway
|
id: start-mcp-gateway
|
||||||
env:
|
env:
|
||||||
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
|
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
|
||||||
GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
|
GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
|
||||||
GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
|
GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
|
||||||
GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
@ -536,7 +523,7 @@ jobs:
|
|||||||
export DEBUG="*"
|
export DEBUG="*"
|
||||||
|
|
||||||
export GH_AW_ENGINE="copilot"
|
export GH_AW_ENGINE="copilot"
|
||||||
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6'
|
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.20'
|
||||||
|
|
||||||
mkdir -p /home/runner/.copilot
|
mkdir -p /home/runner/.copilot
|
||||||
cat << GH_AW_MCP_CONFIG_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh
|
cat << GH_AW_MCP_CONFIG_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh
|
||||||
@ -553,8 +540,6 @@ jobs:
|
|||||||
},
|
},
|
||||||
"guard-policies": {
|
"guard-policies": {
|
||||||
"allow-only": {
|
"allow-only": {
|
||||||
"approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }},
|
|
||||||
"blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }},
|
|
||||||
"min-integrity": "unapproved",
|
"min-integrity": "unapproved",
|
||||||
"repos": "all"
|
"repos": "all"
|
||||||
}
|
}
|
||||||
@ -599,7 +584,7 @@ jobs:
|
|||||||
set -o pipefail
|
set -o pipefail
|
||||||
touch /tmp/gh-aw/agent-step-summary.md
|
touch /tmp/gh-aw/agent-step-summary.md
|
||||||
# shellcheck disable=SC1003
|
# shellcheck disable=SC1003
|
||||||
sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.0 --skip-pull --enable-api-proxy \
|
sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.5 --skip-pull --enable-api-proxy \
|
||||||
-- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
|
-- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
|
||||||
env:
|
env:
|
||||||
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
|
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
|
||||||
@ -608,8 +593,8 @@ jobs:
|
|||||||
GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
|
GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
|
||||||
GH_AW_PHASE: agent
|
GH_AW_PHASE: agent
|
||||||
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
||||||
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
|
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
|
||||||
GH_AW_VERSION: v0.64.0
|
GH_AW_VERSION: v0.62.5
|
||||||
GITHUB_API_URL: ${{ github.api_url }}
|
GITHUB_API_URL: ${{ github.api_url }}
|
||||||
GITHUB_AW: true
|
GITHUB_AW: true
|
||||||
GITHUB_HEAD_REF: ${{ github.head_ref }}
|
GITHUB_HEAD_REF: ${{ github.head_ref }}
|
||||||
@ -686,8 +671,6 @@ jobs:
|
|||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh
|
run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh
|
||||||
- name: Copy Safe Outputs
|
- name: Copy Safe Outputs
|
||||||
if: always()
|
if: always()
|
||||||
env:
|
|
||||||
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/gh-aw
|
mkdir -p /tmp/gh-aw
|
||||||
cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
|
cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
|
||||||
@ -696,7 +679,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
env:
|
env:
|
||||||
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
|
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
|
||||||
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
|
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
|
||||||
GH_AW_ALLOWED_GITHUB_REFS: ""
|
GH_AW_ALLOWED_GITHUB_REFS: ""
|
||||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||||
@ -742,16 +725,10 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo 'AWF binary not installed, skipping firewall log summary'
|
echo 'AWF binary not installed, skipping firewall log summary'
|
||||||
fi
|
fi
|
||||||
- name: Write agent output placeholder if missing
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ ! -f /tmp/gh-aw/agent_output.json ]; then
|
|
||||||
echo '{"items":[]}' > /tmp/gh-aw/agent_output.json
|
|
||||||
fi
|
|
||||||
- name: Upload agent artifacts
|
- name: Upload agent artifacts
|
||||||
if: always()
|
if: always()
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: agent
|
name: agent
|
||||||
path: |
|
path: |
|
||||||
@ -759,168 +736,19 @@ jobs:
|
|||||||
/tmp/gh-aw/sandbox/agent/logs/
|
/tmp/gh-aw/sandbox/agent/logs/
|
||||||
/tmp/gh-aw/redacted-urls.log
|
/tmp/gh-aw/redacted-urls.log
|
||||||
/tmp/gh-aw/mcp-logs/
|
/tmp/gh-aw/mcp-logs/
|
||||||
/tmp/gh-aw/proxy-logs/
|
/tmp/gh-aw/sandbox/firewall/logs/
|
||||||
!/tmp/gh-aw/proxy-logs/proxy-tls/
|
|
||||||
/tmp/gh-aw/agent-stdio.log
|
/tmp/gh-aw/agent-stdio.log
|
||||||
/tmp/gh-aw/agent/
|
/tmp/gh-aw/agent/
|
||||||
/tmp/gh-aw/safeoutputs.jsonl
|
/tmp/gh-aw/safeoutputs.jsonl
|
||||||
/tmp/gh-aw/agent_output.json
|
/tmp/gh-aw/agent_output.json
|
||||||
/tmp/gh-aw/aw-*.patch
|
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
- name: Upload firewall audit logs
|
# --- Threat Detection (inline) ---
|
||||||
if: always()
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
|
||||||
with:
|
|
||||||
name: firewall-audit-logs
|
|
||||||
path: |
|
|
||||||
/tmp/gh-aw/sandbox/firewall/logs/
|
|
||||||
/tmp/gh-aw/sandbox/firewall/audit/
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
conclusion:
|
|
||||||
needs:
|
|
||||||
- activation
|
|
||||||
- agent
|
|
||||||
- detection
|
|
||||||
- safe_outputs
|
|
||||||
if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true')
|
|
||||||
runs-on: ubuntu-slim
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
discussions: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
concurrency:
|
|
||||||
group: "gh-aw-conclusion-pr-ai-slop-review"
|
|
||||||
cancel-in-progress: false
|
|
||||||
outputs:
|
|
||||||
noop_message: ${{ steps.noop.outputs.noop_message }}
|
|
||||||
tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
|
|
||||||
total_count: ${{ steps.missing_tool.outputs.total_count }}
|
|
||||||
steps:
|
|
||||||
- name: Setup Scripts
|
|
||||||
uses: github/gh-aw-actions/setup@63aa903fe409698e15e5718ad89366a72bfe6a89 # v0.65.5
|
|
||||||
with:
|
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
|
||||||
- name: Download agent output artifact
|
|
||||||
id: download-agent-output
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: agent
|
|
||||||
path: /tmp/gh-aw/
|
|
||||||
- name: Setup agent output environment variable
|
|
||||||
id: setup-agent-output-env
|
|
||||||
if: steps.download-agent-output.outcome == 'success'
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/gh-aw/
|
|
||||||
find "/tmp/gh-aw/" -type f -print
|
|
||||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Process No-Op Messages
|
|
||||||
id: noop
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
||||||
env:
|
|
||||||
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
|
|
||||||
GH_AW_NOOP_MAX: "1"
|
|
||||||
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
|
||||||
setupGlobals(core, github, context, exec, io);
|
|
||||||
const { main } = require('${{ runner.temp }}/gh-aw/actions/noop.cjs');
|
|
||||||
await main();
|
|
||||||
- name: Record Missing Tool
|
|
||||||
id: missing_tool
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
||||||
env:
|
|
||||||
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
|
|
||||||
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
|
||||||
setupGlobals(core, github, context, exec, io);
|
|
||||||
const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs');
|
|
||||||
await main();
|
|
||||||
- name: Handle Agent Failure
|
|
||||||
id: handle_agent_failure
|
|
||||||
if: always()
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
||||||
env:
|
|
||||||
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
|
|
||||||
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
|
||||||
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
||||||
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
|
|
||||||
GH_AW_WORKFLOW_ID: "pr-ai-slop-review"
|
|
||||||
GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
|
|
||||||
GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
|
|
||||||
GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
|
|
||||||
GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
|
|
||||||
GH_AW_GROUP_REPORTS: "false"
|
|
||||||
GH_AW_FAILURE_REPORT_AS_ISSUE: "true"
|
|
||||||
GH_AW_TIMEOUT_MINUTES: "20"
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
|
||||||
setupGlobals(core, github, context, exec, io);
|
|
||||||
const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs');
|
|
||||||
await main();
|
|
||||||
- name: Handle No-Op Message
|
|
||||||
id: handle_noop_message
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
||||||
env:
|
|
||||||
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
|
|
||||||
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
|
||||||
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
||||||
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
|
|
||||||
GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }}
|
|
||||||
GH_AW_NOOP_REPORT_AS_ISSUE: "true"
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
|
||||||
setupGlobals(core, github, context, exec, io);
|
|
||||||
const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs');
|
|
||||||
await main();
|
|
||||||
|
|
||||||
detection:
|
|
||||||
needs: agent
|
|
||||||
if: always() && needs.agent.result != 'skipped'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
|
|
||||||
detection_success: ${{ steps.detection_conclusion.outputs.success }}
|
|
||||||
steps:
|
|
||||||
- name: Setup Scripts
|
|
||||||
uses: github/gh-aw-actions/setup@63aa903fe409698e15e5718ad89366a72bfe6a89 # v0.65.5
|
|
||||||
with:
|
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
|
||||||
- name: Download agent output artifact
|
|
||||||
id: download-agent-output
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: agent
|
|
||||||
path: /tmp/gh-aw/
|
|
||||||
- name: Setup agent output environment variable
|
|
||||||
id: setup-agent-output-env
|
|
||||||
if: steps.download-agent-output.outcome == 'success'
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/gh-aw/
|
|
||||||
find "/tmp/gh-aw/" -type f -print
|
|
||||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
|
||||||
# --- Threat Detection ---
|
|
||||||
- name: Download container images
|
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.0 ghcr.io/github/gh-aw-firewall/squid:0.25.0
|
|
||||||
- name: Check if detection needed
|
- name: Check if detection needed
|
||||||
id: detection_guard
|
id: detection_guard
|
||||||
if: always()
|
if: always()
|
||||||
env:
|
env:
|
||||||
OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }}
|
OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }}
|
||||||
HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
|
HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
|
||||||
run: |
|
run: |
|
||||||
if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
|
if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
|
||||||
echo "run_detection=true" >> "$GITHUB_OUTPUT"
|
echo "run_detection=true" >> "$GITHUB_OUTPUT"
|
||||||
@ -952,7 +780,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
WORKFLOW_NAME: "PR AI Slop Review"
|
WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
WORKFLOW_DESCRIPTION: "Reviews incoming pull requests for missing issue linkage and high-confidence\nsigns of one-shot AI-generated changes, then posts a maintainer-focused\ncomment when the risk is high enough to warrant follow-up."
|
WORKFLOW_DESCRIPTION: "Reviews incoming pull requests for missing issue linkage and high-confidence\nsigns of one-shot AI-generated changes, then posts a maintainer-focused\ncomment when the risk is high enough to warrant follow-up."
|
||||||
HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
|
HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
||||||
@ -964,12 +792,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/gh-aw/threat-detection
|
mkdir -p /tmp/gh-aw/threat-detection
|
||||||
touch /tmp/gh-aw/threat-detection/detection.log
|
touch /tmp/gh-aw/threat-detection/detection.log
|
||||||
- name: Install GitHub Copilot CLI
|
|
||||||
run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest
|
|
||||||
env:
|
|
||||||
GH_HOST: github.com
|
|
||||||
- name: Install AWF binary
|
|
||||||
run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.0
|
|
||||||
- name: Execute GitHub Copilot CLI
|
- name: Execute GitHub Copilot CLI
|
||||||
if: always() && steps.detection_guard.outputs.run_detection == 'true'
|
if: always() && steps.detection_guard.outputs.run_detection == 'true'
|
||||||
id: detection_agentic_execution
|
id: detection_agentic_execution
|
||||||
@ -986,15 +808,15 @@ jobs:
|
|||||||
set -o pipefail
|
set -o pipefail
|
||||||
touch /tmp/gh-aw/agent-step-summary.md
|
touch /tmp/gh-aw/agent-step-summary.md
|
||||||
# shellcheck disable=SC1003
|
# shellcheck disable=SC1003
|
||||||
sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.0 --skip-pull --enable-api-proxy \
|
sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.5 --skip-pull --enable-api-proxy \
|
||||||
-- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
|
-- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
|
||||||
env:
|
env:
|
||||||
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
|
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
|
||||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||||
COPILOT_MODEL: gpt-5.1-codex-mini
|
COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
|
||||||
GH_AW_PHASE: detection
|
GH_AW_PHASE: detection
|
||||||
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
|
||||||
GH_AW_VERSION: v0.64.0
|
GH_AW_VERSION: v0.62.5
|
||||||
GITHUB_API_URL: ${{ github.api_url }}
|
GITHUB_API_URL: ${{ github.api_url }}
|
||||||
GITHUB_AW: true
|
GITHUB_AW: true
|
||||||
GITHUB_HEAD_REF: ${{ github.head_ref }}
|
GITHUB_HEAD_REF: ${{ github.head_ref }}
|
||||||
@ -1007,31 +829,153 @@ jobs:
|
|||||||
GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
|
GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
|
||||||
GIT_COMMITTER_NAME: github-actions[bot]
|
GIT_COMMITTER_NAME: github-actions[bot]
|
||||||
XDG_CONFIG_HOME: /home/runner
|
XDG_CONFIG_HOME: /home/runner
|
||||||
- name: Upload threat detection log
|
- name: Parse threat detection results
|
||||||
|
id: parse_detection_results
|
||||||
if: always() && steps.detection_guard.outputs.run_detection == 'true'
|
if: always() && steps.detection_guard.outputs.run_detection == 'true'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
|
||||||
with:
|
|
||||||
name: detection
|
|
||||||
path: /tmp/gh-aw/threat-detection/detection.log
|
|
||||||
if-no-files-found: ignore
|
|
||||||
- name: Parse and conclude threat detection
|
|
||||||
id: detection_conclusion
|
|
||||||
if: always()
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
env:
|
|
||||||
RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
|
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
||||||
setupGlobals(core, github, context, exec, io);
|
setupGlobals(core, github, context, exec, io);
|
||||||
const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs');
|
const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs');
|
||||||
await main();
|
await main();
|
||||||
|
- name: Upload threat detection log
|
||||||
|
if: always() && steps.detection_guard.outputs.run_detection == 'true'
|
||||||
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
|
with:
|
||||||
|
name: detection
|
||||||
|
path: /tmp/gh-aw/threat-detection/detection.log
|
||||||
|
if-no-files-found: ignore
|
||||||
|
- name: Set detection conclusion
|
||||||
|
id: detection_conclusion
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
|
||||||
|
DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }}
|
||||||
|
run: |
|
||||||
|
if [[ "$RUN_DETECTION" != "true" ]]; then
|
||||||
|
echo "conclusion=skipped" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Detection was not needed, marking as skipped"
|
||||||
|
elif [[ "$DETECTION_SUCCESS" == "true" ]]; then
|
||||||
|
echo "conclusion=success" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Detection passed successfully"
|
||||||
|
else
|
||||||
|
echo "conclusion=failure" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "success=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Detection found issues"
|
||||||
|
fi
|
||||||
|
|
||||||
|
conclusion:
|
||||||
|
needs:
|
||||||
|
- activation
|
||||||
|
- agent
|
||||||
|
- safe_outputs
|
||||||
|
if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true')
|
||||||
|
runs-on: ubuntu-slim
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
discussions: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
concurrency:
|
||||||
|
group: "gh-aw-conclusion-pr-ai-slop-review"
|
||||||
|
cancel-in-progress: false
|
||||||
|
outputs:
|
||||||
|
noop_message: ${{ steps.noop.outputs.noop_message }}
|
||||||
|
tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
|
||||||
|
total_count: ${{ steps.missing_tool.outputs.total_count }}
|
||||||
|
steps:
|
||||||
|
- name: Setup Scripts
|
||||||
|
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
||||||
|
with:
|
||||||
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
|
- name: Download agent output artifact
|
||||||
|
id: download-agent-output
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: agent
|
||||||
|
path: /tmp/gh-aw/
|
||||||
|
- name: Setup agent output environment variable
|
||||||
|
if: steps.download-agent-output.outcome == 'success'
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/gh-aw/
|
||||||
|
find "/tmp/gh-aw/" -type f -print
|
||||||
|
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
|
||||||
|
- name: Process No-Op Messages
|
||||||
|
id: noop
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
env:
|
||||||
|
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
|
||||||
|
GH_AW_NOOP_MAX: "1"
|
||||||
|
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
||||||
|
setupGlobals(core, github, context, exec, io);
|
||||||
|
const { main } = require('${{ runner.temp }}/gh-aw/actions/noop.cjs');
|
||||||
|
await main();
|
||||||
|
- name: Record Missing Tool
|
||||||
|
id: missing_tool
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
env:
|
||||||
|
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
|
||||||
|
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
||||||
|
setupGlobals(core, github, context, exec, io);
|
||||||
|
const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs');
|
||||||
|
await main();
|
||||||
|
- name: Handle Agent Failure
|
||||||
|
id: handle_agent_failure
|
||||||
|
if: always()
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
env:
|
||||||
|
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
|
||||||
|
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
|
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
|
||||||
|
GH_AW_WORKFLOW_ID: "pr-ai-slop-review"
|
||||||
|
GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
|
||||||
|
GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
|
||||||
|
GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
|
||||||
|
GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
|
||||||
|
GH_AW_GROUP_REPORTS: "false"
|
||||||
|
GH_AW_FAILURE_REPORT_AS_ISSUE: "true"
|
||||||
|
GH_AW_TIMEOUT_MINUTES: "20"
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
||||||
|
setupGlobals(core, github, context, exec, io);
|
||||||
|
const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs');
|
||||||
|
await main();
|
||||||
|
- name: Handle No-Op Message
|
||||||
|
id: handle_noop_message
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
env:
|
||||||
|
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
|
||||||
|
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
|
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
|
||||||
|
GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }}
|
||||||
|
GH_AW_NOOP_REPORT_AS_ISSUE: "true"
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
|
||||||
|
setupGlobals(core, github, context, exec, io);
|
||||||
|
const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs');
|
||||||
|
await main();
|
||||||
|
|
||||||
safe_outputs:
|
safe_outputs:
|
||||||
needs:
|
needs: agent
|
||||||
- agent
|
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.agent.outputs.detection_success == 'true'
|
||||||
- detection
|
|
||||||
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
|
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@ -1042,7 +986,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/pr-ai-slop-review"
|
GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/pr-ai-slop-review"
|
||||||
GH_AW_ENGINE_ID: "copilot"
|
GH_AW_ENGINE_ID: "copilot"
|
||||||
GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }}
|
|
||||||
GH_AW_WORKFLOW_ID: "pr-ai-slop-review"
|
GH_AW_WORKFLOW_ID: "pr-ai-slop-review"
|
||||||
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
GH_AW_WORKFLOW_NAME: "PR AI Slop Review"
|
||||||
outputs:
|
outputs:
|
||||||
@ -1056,7 +999,7 @@ jobs:
|
|||||||
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
|
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
uses: github/gh-aw-actions/setup@63aa903fe409698e15e5718ad89366a72bfe6a89 # v0.65.5
|
uses: github/gh-aw-actions/setup@742ca9c12baa13667ac53db8eb95f48414f60792 # v0.65.7
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
- name: Download agent output artifact
|
- name: Download agent output artifact
|
||||||
@ -1067,26 +1010,24 @@ jobs:
|
|||||||
name: agent
|
name: agent
|
||||||
path: /tmp/gh-aw/
|
path: /tmp/gh-aw/
|
||||||
- name: Setup agent output environment variable
|
- name: Setup agent output environment variable
|
||||||
id: setup-agent-output-env
|
|
||||||
if: steps.download-agent-output.outcome == 'success'
|
if: steps.download-agent-output.outcome == 'success'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/gh-aw/
|
mkdir -p /tmp/gh-aw/
|
||||||
find "/tmp/gh-aw/" -type f -print
|
find "/tmp/gh-aw/" -type f -print
|
||||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
|
||||||
- name: Configure GH_HOST for enterprise compatibility
|
- name: Configure GH_HOST for enterprise compatibility
|
||||||
id: ghes-host-config
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
|
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
|
||||||
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
|
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
|
||||||
GH_HOST="${GITHUB_SERVER_URL#https://}"
|
GH_HOST="${GITHUB_SERVER_URL#https://}"
|
||||||
GH_HOST="${GH_HOST#http://}"
|
GH_HOST="${GH_HOST#http://}"
|
||||||
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_OUTPUT"
|
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
|
||||||
- name: Process Safe Outputs
|
- name: Process Safe Outputs
|
||||||
id: process_safe_outputs
|
id: process_safe_outputs
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
env:
|
env:
|
||||||
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
|
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
|
||||||
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
|
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
|
||||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||||
GITHUB_API_URL: ${{ github.api_url }}
|
GITHUB_API_URL: ${{ github.api_url }}
|
||||||
@ -1098,9 +1039,9 @@ jobs:
|
|||||||
setupGlobals(core, github, context, exec, io);
|
setupGlobals(core, github, context, exec, io);
|
||||||
const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs');
|
const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs');
|
||||||
await main();
|
await main();
|
||||||
- name: Upload Safe Output Items
|
- name: Upload safe output items
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: safe-output-items
|
name: safe-output-items
|
||||||
path: /tmp/gh-aw/safe-output-items.jsonl
|
path: /tmp/gh-aw/safe-output-items.jsonl
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ target
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
.vfox.toml
|
.vfox.toml
|
||||||
.vfox/
|
.vfox/
|
||||||
|
.claude
|
||||||
|
|||||||
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -559,16 +559,13 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backoff"
|
name = "backon"
|
||||||
version = "0.4.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
|
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"fastrand 2.3.0",
|
||||||
"getrandom 0.2.17",
|
"gloo-timers",
|
||||||
"instant",
|
|
||||||
"pin-project-lite",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1117,7 +1114,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"backoff",
|
"backon",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"boa_engine",
|
"boa_engine",
|
||||||
@ -1243,7 +1240,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "clash_verge_service_ipc"
|
name = "clash_verge_service_ipc"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#b73568a9ecc9e62577e9ce81a123b554f06a9fb3"
|
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#62e0fe76279350303373e13cbdb6af32a04abe0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
@ -3039,6 +3036,18 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export default defineConfig([
|
|||||||
'@eslint-react/no-children-for-each': 'error',
|
'@eslint-react/no-children-for-each': 'error',
|
||||||
'@eslint-react/no-children-map': 'error',
|
'@eslint-react/no-children-map': 'error',
|
||||||
'@eslint-react/no-children-only': 'error',
|
'@eslint-react/no-children-only': 'error',
|
||||||
'@eslint-react/no-children-prop': 'error',
|
'@eslint-react/jsx-no-children-prop': 'error',
|
||||||
'@eslint-react/no-children-to-array': 'error',
|
'@eslint-react/no-children-to-array': 'error',
|
||||||
'@eslint-react/no-class-component': 'error',
|
'@eslint-react/no-class-component': 'error',
|
||||||
'@eslint-react/no-clone-element': 'error',
|
'@eslint-react/no-clone-element': 'error',
|
||||||
@ -86,7 +86,7 @@ export default defineConfig([
|
|||||||
'@eslint-react/no-unstable-default-props': 'warn',
|
'@eslint-react/no-unstable-default-props': 'warn',
|
||||||
'@eslint-react/no-unused-class-component-members': 'error',
|
'@eslint-react/no-unused-class-component-members': 'error',
|
||||||
'@eslint-react/no-unused-state': 'error',
|
'@eslint-react/no-unused-state': 'error',
|
||||||
'@eslint-react/no-useless-fragment': 'warn',
|
'@eslint-react/jsx-no-useless-fragment': 'warn',
|
||||||
'@eslint-react/prefer-destructuring-assignment': 'warn',
|
'@eslint-react/prefer-destructuring-assignment': 'warn',
|
||||||
|
|
||||||
// TypeScript
|
// TypeScript
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"@mui/icons-material": "^7.3.9",
|
"@mui/icons-material": "^7.3.9",
|
||||||
"@mui/lab": "7.0.0-beta.17",
|
"@mui/lab": "7.0.0-beta.17",
|
||||||
"@mui/material": "^7.3.9",
|
"@mui/material": "^7.3.9",
|
||||||
|
"@tanstack/react-query": "^5.96.1",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tanstack/react-virtual": "^3.13.23",
|
"@tanstack/react-virtual": "^3.13.23",
|
||||||
"@tauri-apps/api": "2.10.1",
|
"@tauri-apps/api": "2.10.1",
|
||||||
@ -55,7 +56,6 @@
|
|||||||
"@tauri-apps/plugin-shell": "2.3.5",
|
"@tauri-apps/plugin-shell": "2.3.5",
|
||||||
"@tauri-apps/plugin-updater": "2.10.0",
|
"@tauri-apps/plugin-updater": "2.10.0",
|
||||||
"ahooks": "^3.9.6",
|
"ahooks": "^3.9.6",
|
||||||
"axios": "^1.13.6",
|
|
||||||
"cidr-block": "^2.3.0",
|
"cidr-block": "^2.3.0",
|
||||||
"dayjs": "1.11.20",
|
"dayjs": "1.11.20",
|
||||||
"foxact": "^0.3.0",
|
"foxact": "^0.3.0",
|
||||||
@ -74,9 +74,7 @@
|
|||||||
"react-i18next": "17.0.2",
|
"react-i18next": "17.0.2",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router": "^7.13.1",
|
"react-router": "^7.13.1",
|
||||||
"react-virtuoso": "^4.18.3",
|
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"swr": "^2.4.1",
|
|
||||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#revert",
|
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#revert",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"validator": "^13.15.26"
|
"validator": "^13.15.26"
|
||||||
@ -94,6 +92,7 @@
|
|||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
"@vitejs/plugin-legacy": "^8.0.0",
|
"@vitejs/plugin-legacy": "^8.0.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"axios": "^1.13.6",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
|
|||||||
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
@ -38,6 +38,9 @@ importers:
|
|||||||
'@mui/material':
|
'@mui/material':
|
||||||
specifier: ^7.3.9
|
specifier: ^7.3.9
|
||||||
version: 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@tanstack/react-query':
|
||||||
|
specifier: ^5.96.1
|
||||||
|
version: 5.96.1(react@19.2.4)
|
||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.21.3
|
specifier: ^8.21.3
|
||||||
version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@ -71,9 +74,6 @@ importers:
|
|||||||
ahooks:
|
ahooks:
|
||||||
specifier: ^3.9.6
|
specifier: ^3.9.6
|
||||||
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
axios:
|
|
||||||
specifier: ^1.13.6
|
|
||||||
version: 1.14.0
|
|
||||||
cidr-block:
|
cidr-block:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
@ -128,15 +128,9 @@ importers:
|
|||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.13.1
|
specifier: ^7.13.1
|
||||||
version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react-virtuoso:
|
|
||||||
specifier: ^4.18.3
|
|
||||||
version: 4.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
||||||
rehype-raw:
|
rehype-raw:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
swr:
|
|
||||||
specifier: ^2.4.1
|
|
||||||
version: 2.4.1(react@19.2.4)
|
|
||||||
tauri-plugin-mihomo-api:
|
tauri-plugin-mihomo-api:
|
||||||
specifier: github:clash-verge-rev/tauri-plugin-mihomo#revert
|
specifier: github:clash-verge-rev/tauri-plugin-mihomo#revert
|
||||||
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1cc80bc0fbe1245315617f4cecd93710a152325b
|
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1cc80bc0fbe1245315617f4cecd93710a152325b
|
||||||
@ -186,6 +180,9 @@ importers:
|
|||||||
adm-zip:
|
adm-zip:
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
|
axios:
|
||||||
|
specifier: ^1.13.6
|
||||||
|
version: 1.14.0
|
||||||
cli-color:
|
cli-color:
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
@ -1434,6 +1431,14 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@svgr/core': '*'
|
'@svgr/core': '*'
|
||||||
|
|
||||||
|
'@tanstack/query-core@5.96.1':
|
||||||
|
resolution: {integrity: sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==}
|
||||||
|
|
||||||
|
'@tanstack/react-query@5.96.1':
|
||||||
|
resolution: {integrity: sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19
|
||||||
|
|
||||||
'@tanstack/react-table@8.21.3':
|
'@tanstack/react-table@8.21.3':
|
||||||
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
|
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -3161,12 +3166,6 @@ packages:
|
|||||||
react: '>=16.6.0'
|
react: '>=16.6.0'
|
||||||
react-dom: '>=16.6.0'
|
react-dom: '>=16.6.0'
|
||||||
|
|
||||||
react-virtuoso@4.18.3:
|
|
||||||
resolution: {integrity: sha512-fLz/peHAx4Eu0DLHurFEEI7Y6n5CqEoxBh04rgJM9yMuOJah2a9zWg/MUOmZLcp7zuWYorXq5+5bf3IRgkNvWg==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16 || >=17 || >= 18 || >= 19'
|
|
||||||
react-dom: '>=16 || >=17 || >= 18 || >=19'
|
|
||||||
|
|
||||||
react@19.2.4:
|
react@19.2.4:
|
||||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -3345,11 +3344,6 @@ packages:
|
|||||||
svg-parser@2.0.4:
|
svg-parser@2.0.4:
|
||||||
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
|
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
|
||||||
|
|
||||||
swr@2.4.1:
|
|
||||||
resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
systemjs@6.15.1:
|
systemjs@6.15.1:
|
||||||
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
|
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
|
||||||
|
|
||||||
@ -4963,6 +4957,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@tanstack/query-core@5.96.1': {}
|
||||||
|
|
||||||
|
'@tanstack/react-query@5.96.1(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/query-core': 5.96.1
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
'@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/table-core': 8.21.3
|
'@tanstack/table-core': 8.21.3
|
||||||
@ -6868,11 +6869,6 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
react-virtuoso@4.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
|
||||||
dependencies:
|
|
||||||
react: 19.2.4
|
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
|
||||||
|
|
||||||
react@19.2.4: {}
|
react@19.2.4: {}
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
@ -7064,12 +7060,6 @@ snapshots:
|
|||||||
|
|
||||||
svg-parser@2.0.4: {}
|
svg-parser@2.0.4: {}
|
||||||
|
|
||||||
swr@2.4.1(react@19.2.4):
|
|
||||||
dependencies:
|
|
||||||
dequal: 2.0.3
|
|
||||||
react: 19.2.4
|
|
||||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
|
||||||
|
|
||||||
systemjs@6.15.1: {}
|
systemjs@6.15.1: {}
|
||||||
|
|
||||||
tar@7.5.13:
|
tar@7.5.13:
|
||||||
|
|||||||
@ -91,7 +91,7 @@ gethostname = "1.1.0"
|
|||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
tauri-plugin-notification = "2.3.3"
|
tauri-plugin-notification = "2.3.3"
|
||||||
tokio-stream = "0.1.18"
|
tokio-stream = "0.1.18"
|
||||||
backoff = { version = "0.4.0", features = ["tokio"] }
|
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||||
tauri-plugin-http = "2.5.7"
|
tauri-plugin-http = "2.5.7"
|
||||||
console-subscriber = { version = "0.5.0", optional = true }
|
console-subscriber = { version = "0.5.0", optional = true }
|
||||||
tauri-plugin-devtools = { version = "2.0.1" }
|
tauri-plugin-devtools = { version = "2.0.1" }
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use crate::{
|
|||||||
utils::{dirs, help},
|
utils::{dirs, help},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
use backon::{ExponentialBuilder, Retryable as _};
|
||||||
use clash_verge_draft::Draft;
|
use clash_verge_draft::Draft;
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging, logging_error};
|
||||||
use serde_yaml_ng::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
@ -204,23 +204,21 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_config_initialization() {
|
pub async fn verify_config_initialization() {
|
||||||
let backoff_strategy = ExponentialBackoff {
|
let backoff = ExponentialBuilder::default()
|
||||||
initial_interval: std::time::Duration::from_millis(100),
|
.with_min_delay(std::time::Duration::from_millis(100))
|
||||||
max_interval: std::time::Duration::from_secs(2),
|
.with_max_delay(std::time::Duration::from_secs(2))
|
||||||
max_elapsed_time: Some(std::time::Duration::from_secs(10)),
|
.with_factor(2.0)
|
||||||
multiplier: 2.0,
|
.with_max_times(10);
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let operation = || async {
|
if let Err(e) = (|| async {
|
||||||
if Self::runtime().await.latest_arc().config.is_some() {
|
if Self::runtime().await.latest_arc().config.is_some() {
|
||||||
return Ok::<(), BackoffError<anyhow::Error>>(());
|
return Ok::<(), anyhow::Error>(());
|
||||||
}
|
}
|
||||||
|
Self::generate().await
|
||||||
Self::generate().await.map_err(BackoffError::transient)
|
})
|
||||||
};
|
.retry(backoff)
|
||||||
|
.await
|
||||||
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
|
{
|
||||||
logging!(error, Type::Setup, "Config init verification failed: {}", e);
|
logging!(error, Type::Setup, "Config init verification failed: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ use crate::constants::files::DNS_CONFIG;
|
|||||||
use crate::{config::Config, process::AsyncHandler, utils::dirs};
|
use crate::{config::Config, process::AsyncHandler, utils::dirs};
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||||
|
use backon::{ConstantBuilder, Retryable as _};
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||||
@ -166,40 +167,25 @@ impl WebDavClient {
|
|||||||
let client = self.get_client(Operation::Upload).await?;
|
let client = self.get_client(Operation::Upload).await?;
|
||||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
|
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
|
||||||
|
|
||||||
// 读取文件并上传,如果失败尝试一次重试
|
|
||||||
let file_content = fs::read(&file_path).await?;
|
let file_content = fs::read(&file_path).await?;
|
||||||
|
|
||||||
// 添加超时保护
|
let backoff = ConstantBuilder::default()
|
||||||
let upload_result = timeout(
|
.with_delay(Duration::from_millis(500))
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
.with_max_times(1);
|
||||||
client.put(&webdav_path, file_content.clone()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match upload_result {
|
(|| async {
|
||||||
Err(_) => {
|
timeout(
|
||||||
logging!(warn, Type::Backup, "Warning: Upload timed out, retrying once");
|
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
client.put(&webdav_path, file_content.clone()),
|
||||||
timeout(
|
)
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
.await??;
|
||||||
client.put(&webdav_path, file_content),
|
Ok::<(), Error>(())
|
||||||
)
|
})
|
||||||
.await??;
|
.retry(backoff)
|
||||||
Ok(())
|
.notify(|err, dur| {
|
||||||
}
|
logging!(warn, Type::Backup, "Upload failed: {err}, retrying in {dur:?}");
|
||||||
|
})
|
||||||
Ok(Err(e)) => {
|
.await
|
||||||
logging!(warn, Type::Backup, "Warning: Upload failed, retrying once: {e}");
|
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
||||||
timeout(
|
|
||||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
|
||||||
client.put(&webdav_path, file_content),
|
|
||||||
)
|
|
||||||
.await??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(Ok(_)) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
||||||
|
|||||||
@ -84,7 +84,7 @@ impl CoreManager {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn wait_for_service_if_needed(&self) {
|
async fn wait_for_service_if_needed(&self) {
|
||||||
use crate::{config::Config, constants::timing, core::service};
|
use crate::{config::Config, constants::timing, core::service};
|
||||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
use backon::{ConstantBuilder, Retryable as _};
|
||||||
|
|
||||||
let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
|
let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
|
||||||
|
|
||||||
@ -92,16 +92,12 @@ impl CoreManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let backoff = ExponentialBackoff {
|
let max_times = timing::SERVICE_WAIT_MAX.as_millis() / timing::SERVICE_WAIT_INTERVAL.as_millis();
|
||||||
initial_interval: timing::SERVICE_WAIT_INTERVAL,
|
let backoff = ConstantBuilder::default()
|
||||||
max_interval: timing::SERVICE_WAIT_INTERVAL,
|
.with_delay(timing::SERVICE_WAIT_INTERVAL)
|
||||||
max_elapsed_time: Some(timing::SERVICE_WAIT_MAX),
|
.with_max_times(max_times as usize);
|
||||||
multiplier: 1.0,
|
|
||||||
randomization_factor: 0.0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let operation = || async {
|
let _ = (|| async {
|
||||||
let mut manager = SERVICE_MANAGER.lock().await;
|
let mut manager = SERVICE_MANAGER.lock().await;
|
||||||
|
|
||||||
if matches!(manager.current(), ServiceStatus::Ready) {
|
if matches!(manager.current(), ServiceStatus::Ready) {
|
||||||
@ -111,19 +107,19 @@ impl CoreManager {
|
|||||||
// If the service IPC path is not ready yet, treat it as transient and retry.
|
// If the service IPC path is not ready yet, treat it as transient and retry.
|
||||||
// Running init/refresh too early can mark service state unavailable and break later config reloads.
|
// Running init/refresh too early can mark service state unavailable and break later config reloads.
|
||||||
if !service::is_service_ipc_path_exists() {
|
if !service::is_service_ipc_path_exists() {
|
||||||
return Err(BackoffError::transient(anyhow::anyhow!("Service IPC not ready")));
|
return Err(anyhow::anyhow!("Service IPC not ready"));
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.init().await.map_err(BackoffError::transient)?;
|
manager.init().await?;
|
||||||
let _ = manager.refresh().await;
|
let _ = manager.refresh().await;
|
||||||
|
|
||||||
if matches!(manager.current(), ServiceStatus::Ready) {
|
if matches!(manager.current(), ServiceStatus::Ready) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(BackoffError::transient(anyhow::anyhow!("Service not ready")))
|
Err(anyhow::anyhow!("Service not ready"))
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
|
.retry(backoff)
|
||||||
let _ = backoff::future::retry(backoff, operation).await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use crate::{
|
|||||||
utils::dirs,
|
utils::dirs,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, Result, anyhow, bail};
|
use anyhow::{Context as _, Result, anyhow, bail};
|
||||||
|
use backon::{ConstantBuilder, Retryable as _};
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging, logging_error};
|
||||||
use clash_verge_service_ipc::CoreConfig;
|
use clash_verge_service_ipc::CoreConfig;
|
||||||
use compact_str::CompactString;
|
use compact_str::CompactString;
|
||||||
@ -15,7 +16,7 @@ use std::{
|
|||||||
process::Command as StdCommand,
|
process::Command as StdCommand,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::{sync::Mutex, time::sleep};
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ServiceStatus {
|
pub enum ServiceStatus {
|
||||||
@ -441,31 +442,27 @@ pub async fn wait_and_check_service_available(status: &mut ServiceManager) -> Re
|
|||||||
async fn wait_for_service_ipc(status: &mut ServiceManager, reason: &str) -> Result<()> {
|
async fn wait_for_service_ipc(status: &mut ServiceManager, reason: &str) -> Result<()> {
|
||||||
status.0 = ServiceStatus::Unavailable(reason.into());
|
status.0 = ServiceStatus::Unavailable(reason.into());
|
||||||
let config = ServiceManager::config();
|
let config = ServiceManager::config();
|
||||||
let mut attempts = 0u32;
|
|
||||||
#[allow(unused_assignments)]
|
|
||||||
let mut last_err = anyhow!("service not ready");
|
|
||||||
|
|
||||||
loop {
|
let backoff = ConstantBuilder::default()
|
||||||
|
.with_delay(config.retry_delay)
|
||||||
|
.with_max_times(config.max_retries);
|
||||||
|
|
||||||
|
let result = (|| async {
|
||||||
if Path::new(clash_verge_service_ipc::IPC_PATH).exists() {
|
if Path::new(clash_verge_service_ipc::IPC_PATH).exists() {
|
||||||
match clash_verge_service_ipc::connect().await {
|
clash_verge_service_ipc::connect().await?;
|
||||||
Ok(_) => {
|
Ok(())
|
||||||
status.0 = ServiceStatus::Ready;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(e) => last_err = e,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
last_err = anyhow!("IPC path not ready");
|
Err(anyhow!("IPC path not ready"))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.retry(backoff)
|
||||||
|
.await;
|
||||||
|
|
||||||
if attempts >= config.max_retries as u32 {
|
if result.is_ok() {
|
||||||
break;
|
status.0 = ServiceStatus::Ready;
|
||||||
}
|
|
||||||
attempts += 1;
|
|
||||||
sleep(config.retry_delay).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(last_err)
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_service_ipc_path_exists() -> bool {
|
pub fn is_service_ipc_path_exists() -> bool {
|
||||||
|
|||||||
@ -155,7 +155,13 @@ impl NetworkManager {
|
|||||||
if !parsed.username().is_empty()
|
if !parsed.username().is_empty()
|
||||||
&& let Some(pass) = parsed.password()
|
&& let Some(pass) = parsed.password()
|
||||||
{
|
{
|
||||||
let auth_str = format!("{}:{}", parsed.username(), pass);
|
let username = percent_encoding::percent_decode_str(parsed.username())
|
||||||
|
.decode_utf8_lossy()
|
||||||
|
.into_owned();
|
||||||
|
let password = percent_encoding::percent_decode_str(pass)
|
||||||
|
.decode_utf8_lossy()
|
||||||
|
.into_owned();
|
||||||
|
let auth_str = format!("{}:{}", username, password);
|
||||||
let encoded = general_purpose::STANDARD.encode(auth_str);
|
let encoded = general_purpose::STANDARD.encode(auth_str);
|
||||||
extra_headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", encoded))?);
|
extra_headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", encoded))?);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,3 +14,4 @@ export { BaseStyledSelect } from './base-styled-select'
|
|||||||
export { BaseStyledTextField } from './base-styled-text-field'
|
export { BaseStyledTextField } from './base-styled-text-field'
|
||||||
export { Switch } from './base-switch'
|
export { Switch } from './base-switch'
|
||||||
export { TooltipIcon } from './base-tooltip-icon'
|
export { TooltipIcon } from './base-tooltip-icon'
|
||||||
|
export { VirtualList, type VirtualListHandle } from './virtual-list'
|
||||||
|
|||||||
97
src/components/base/virtual-list.tsx
Normal file
97
src/components/base/virtual-list.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
forwardRef,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
export interface VirtualListHandle {
|
||||||
|
scrollToIndex: (
|
||||||
|
index: number,
|
||||||
|
options?: {
|
||||||
|
align?: 'start' | 'center' | 'end' | 'auto'
|
||||||
|
behavior?: ScrollBehavior
|
||||||
|
},
|
||||||
|
) => void
|
||||||
|
scrollTo: (options: ScrollToOptions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VirtualListProps {
|
||||||
|
count: number
|
||||||
|
estimateSize: number
|
||||||
|
overscan?: number
|
||||||
|
getItemKey?: (index: number) => React.Key
|
||||||
|
renderItem: (index: number) => ReactNode
|
||||||
|
style?: CSSProperties
|
||||||
|
footer?: number
|
||||||
|
onScroll?: (e: Event) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VirtualList = forwardRef<VirtualListHandle, VirtualListProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
count,
|
||||||
|
estimateSize,
|
||||||
|
overscan = 5,
|
||||||
|
getItemKey,
|
||||||
|
renderItem,
|
||||||
|
style,
|
||||||
|
footer,
|
||||||
|
onScroll,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => estimateSize,
|
||||||
|
overscan,
|
||||||
|
getItemKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = parentRef.current
|
||||||
|
if (!el || !onScroll) return
|
||||||
|
el.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
return () => el.removeEventListener('scroll', onScroll)
|
||||||
|
}, [onScroll])
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
scrollToIndex: (index, options) =>
|
||||||
|
virtualizer.scrollToIndex(index, options),
|
||||||
|
scrollTo: (options) => parentRef.current?.scrollTo(options),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={parentRef} style={{ ...style, overflow: 'auto' }}>
|
||||||
|
<div
|
||||||
|
style={{ height: virtualizer.getTotalSize(), position: 'relative' }}
|
||||||
|
>
|
||||||
|
{virtualizer.getVirtualItems().map((vi) => (
|
||||||
|
<div
|
||||||
|
key={vi.key}
|
||||||
|
data-index={vi.index}
|
||||||
|
ref={virtualizer.measureElement}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
transform: `translateY(${vi.start}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderItem(vi.index)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{footer != null && <div style={{ height: footer }} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
VirtualList.displayName = 'VirtualList'
|
||||||
@ -5,23 +5,22 @@ import {
|
|||||||
VisibilityOutlined,
|
VisibilityOutlined,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
|
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { useEffect } from 'foxact/use-abortable-effect'
|
import { useEffect } from 'foxact/use-abortable-effect'
|
||||||
import { useIntersection } from 'foxact/use-intersection'
|
import { useIntersection } from 'foxact/use-intersection'
|
||||||
import type { XOR } from 'foxts/ts-xor'
|
import type { XOR } from 'foxts/ts-xor'
|
||||||
import {
|
import {
|
||||||
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useState,
|
|
||||||
useEffectEvent,
|
useEffectEvent,
|
||||||
useMemo,
|
useMemo,
|
||||||
forwardRef,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWRImmutable from 'swr/immutable'
|
|
||||||
|
|
||||||
import { getIpInfo } from '@/services/api'
|
import { getIpInfo } from '@/services/api'
|
||||||
import { SWR_EXTERNAL_API } from '@/services/config'
|
|
||||||
|
|
||||||
import { EnhancedCard } from './enhanced-card'
|
import { EnhancedCard } from './enhanced-card'
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ type CountDownState = XOR<
|
|||||||
const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
||||||
({ children }, ref) => {
|
({ children }, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mutate } = useIPInfo()
|
const { refetch: mutate } = useIPInfo()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@ -117,7 +116,7 @@ export const IpInfoCard = () => {
|
|||||||
remainingSeconds: IP_REFRESH_SECONDS,
|
remainingSeconds: IP_REFRESH_SECONDS,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: ipInfo, error, isLoading, mutate } = useIPInfo()
|
const { data: ipInfo, error, isLoading, refetch: mutate } = useIPInfo()
|
||||||
|
|
||||||
// function useEffectEvent
|
// function useEffectEvent
|
||||||
const onCountdownTick = useEffectEvent(async () => {
|
const onCountdownTick = useEffectEvent(async () => {
|
||||||
@ -422,5 +421,14 @@ export const IpInfoCard = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useIPInfo() {
|
function useIPInfo() {
|
||||||
return useSWRImmutable(IP_INFO_CACHE_KEY, getIpInfo, SWR_EXTERNAL_API)
|
return useQuery({
|
||||||
|
queryKey: [IP_INFO_CACHE_KEY],
|
||||||
|
queryFn: getIpInfo,
|
||||||
|
staleTime: Infinity,
|
||||||
|
gcTime: Infinity,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: 1,
|
||||||
|
retryDelay: 30_000,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,7 +101,8 @@ export const SystemInfoCard = () => {
|
|||||||
// 检查更新
|
// 检查更新
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
const info = await triggerCheckUpdate()
|
const result = await triggerCheckUpdate()
|
||||||
|
const info = result.data
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice.success(
|
showNotice.success(
|
||||||
'settings.components.verge.advanced.notifications.latestVersion',
|
'settings.components.verge.advanced.notifications.latestVersion',
|
||||||
|
|||||||
@ -43,9 +43,8 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
|
||||||
|
|
||||||
import { BaseSearchBox, Switch } from '@/components/base'
|
import { BaseSearchBox, Switch, VirtualList } from '@/components/base'
|
||||||
import { GroupItem } from '@/components/profile/group-item'
|
import { GroupItem } from '@/components/profile/group-item'
|
||||||
import {
|
import {
|
||||||
getNetworkInterfaces,
|
getNetworkInterfaces,
|
||||||
@ -185,6 +184,92 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
[appendSeq, match],
|
[appendSeq, match],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderItem = (index: number): React.ReactNode => {
|
||||||
|
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||||
|
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onPrependDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={filteredPrependSeq.map((x) => {
|
||||||
|
return x.name
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filteredPrependSeq.map((item) => {
|
||||||
|
return (
|
||||||
|
<GroupItem
|
||||||
|
key={item.name}
|
||||||
|
type="prepend"
|
||||||
|
group={item}
|
||||||
|
onDelete={() => {
|
||||||
|
setPrependSeq(
|
||||||
|
prependSeq.filter((v) => v.name !== item.name),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
} else if (index < filteredGroupList.length + shift) {
|
||||||
|
const newIndex = index - shift
|
||||||
|
return (
|
||||||
|
<GroupItem
|
||||||
|
key={filteredGroupList[newIndex].name}
|
||||||
|
type={
|
||||||
|
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||||
|
? 'delete'
|
||||||
|
: 'original'
|
||||||
|
}
|
||||||
|
group={filteredGroupList[newIndex]}
|
||||||
|
onDelete={() => {
|
||||||
|
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||||
|
setDeleteSeq(
|
||||||
|
deleteSeq.filter((v) => v !== filteredGroupList[newIndex].name),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setDeleteSeq((prev) => [
|
||||||
|
...prev,
|
||||||
|
filteredGroupList[newIndex].name,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onAppendDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={filteredAppendSeq.map((x) => {
|
||||||
|
return x.name
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filteredAppendSeq.map((item) => {
|
||||||
|
return (
|
||||||
|
<GroupItem
|
||||||
|
key={item.name}
|
||||||
|
type="append"
|
||||||
|
group={item}
|
||||||
|
onDelete={() => {
|
||||||
|
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: { distance: 8 },
|
activationConstraint: { distance: 8 },
|
||||||
@ -1002,109 +1087,15 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
<Virtuoso
|
<VirtualList
|
||||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
count={
|
||||||
totalCount={
|
|
||||||
filteredGroupList.length +
|
filteredGroupList.length +
|
||||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
increaseViewportBy={256}
|
estimateSize={56}
|
||||||
itemContent={(index) => {
|
renderItem={renderItem}
|
||||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onPrependDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredPrependSeq.map((x) => {
|
|
||||||
return x.name
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{filteredPrependSeq.map((item) => {
|
|
||||||
return (
|
|
||||||
<GroupItem
|
|
||||||
key={item.name}
|
|
||||||
type="prepend"
|
|
||||||
group={item}
|
|
||||||
onDelete={() => {
|
|
||||||
setPrependSeq(
|
|
||||||
prependSeq.filter(
|
|
||||||
(v) => v.name !== item.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
} else if (index < filteredGroupList.length + shift) {
|
|
||||||
const newIndex = index - shift
|
|
||||||
return (
|
|
||||||
<GroupItem
|
|
||||||
key={filteredGroupList[newIndex].name}
|
|
||||||
type={
|
|
||||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
|
||||||
? 'delete'
|
|
||||||
: 'original'
|
|
||||||
}
|
|
||||||
group={filteredGroupList[newIndex]}
|
|
||||||
onDelete={() => {
|
|
||||||
if (
|
|
||||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
|
||||||
) {
|
|
||||||
setDeleteSeq(
|
|
||||||
deleteSeq.filter(
|
|
||||||
(v) => v !== filteredGroupList[newIndex].name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setDeleteSeq((prev) => [
|
|
||||||
...prev,
|
|
||||||
filteredGroupList[newIndex].name,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onAppendDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredAppendSeq.map((x) => {
|
|
||||||
return x.name
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{filteredAppendSeq.map((item) => {
|
|
||||||
return (
|
|
||||||
<GroupItem
|
|
||||||
key={item.name}
|
|
||||||
type="append"
|
|
||||||
group={item}
|
|
||||||
onDelete={() => {
|
|
||||||
setAppendSeq(
|
|
||||||
appendSeq.filter(
|
|
||||||
(v) => v.name !== item.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import { useLockFn } from 'ahooks'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
import { useCallback, useEffect, useReducer, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { mutate } from 'swr'
|
|
||||||
|
|
||||||
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
||||||
import { EditorViewer } from '@/components/profile/editor-viewer'
|
import { EditorViewer } from '@/components/profile/editor-viewer'
|
||||||
@ -53,6 +52,7 @@ interface Props {
|
|||||||
selected: boolean
|
selected: boolean
|
||||||
activating: boolean
|
activating: boolean
|
||||||
itemData: IProfileItem
|
itemData: IProfileItem
|
||||||
|
mutateProfiles: () => Promise<void>
|
||||||
onSelect: (force: boolean) => void
|
onSelect: (force: boolean) => void
|
||||||
onEdit: () => void
|
onEdit: () => void
|
||||||
onSave?: (prev?: string, curr?: string) => void
|
onSave?: (prev?: string, curr?: string) => void
|
||||||
@ -68,6 +68,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
selected,
|
selected,
|
||||||
activating,
|
activating,
|
||||||
itemData,
|
itemData,
|
||||||
|
mutateProfiles,
|
||||||
onSelect,
|
onSelect,
|
||||||
onEdit,
|
onEdit,
|
||||||
onSave,
|
onSave,
|
||||||
@ -383,7 +384,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
await updateProfile(itemData.uid, payload)
|
await updateProfile(itemData.uid, payload)
|
||||||
|
|
||||||
// 更新成功,刷新列表
|
// 更新成功,刷新列表
|
||||||
mutate('getProfiles')
|
void mutateProfiles()
|
||||||
} catch {
|
} catch {
|
||||||
// 更新完全失败(包括后端的回退尝试)
|
// 更新完全失败(包括后端的回退尝试)
|
||||||
// 不需要做处理,后端会通过事件通知系统发送错误
|
// 不需要做处理,后端会通过事件通知系统发送错误
|
||||||
@ -579,7 +580,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
if (customEvent.detail?.uid === itemData.uid) {
|
if (customEvent.detail?.uid === itemData.uid) {
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }))
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }))
|
||||||
// 刷新 profile 数据以获取最新的 updated 时间戳
|
// 刷新 profile 数据以获取最新的 updated 时间戳
|
||||||
mutate('getProfiles')
|
void mutateProfiles()
|
||||||
// 更新完成后刷新显示
|
// 更新完成后刷新显示
|
||||||
if (showNextUpdate) {
|
if (showNextUpdate) {
|
||||||
fetchNextUpdateTime()
|
fetchNextUpdateTime()
|
||||||
@ -599,7 +600,13 @@ export const ProfileItem = (props: Props) => {
|
|||||||
handleUpdateCompleted,
|
handleUpdateCompleted,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [fetchNextUpdateTime, itemData.uid, setLoadingCache, showNextUpdate])
|
}, [
|
||||||
|
fetchNextUpdateTime,
|
||||||
|
itemData.uid,
|
||||||
|
mutateProfiles,
|
||||||
|
setLoadingCache,
|
||||||
|
showNextUpdate,
|
||||||
|
])
|
||||||
|
|
||||||
const handleSaveProfileDocument = useLockFn(async () => {
|
const handleSaveProfileDocument = useLockFn(async () => {
|
||||||
const currentValue = profileDocument.value
|
const currentValue = profileDocument.value
|
||||||
|
|||||||
@ -35,9 +35,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
|
||||||
|
|
||||||
import { BaseSearchBox } from '@/components/base'
|
import { BaseSearchBox, VirtualList } from '@/components/base'
|
||||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
@ -81,6 +80,92 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
[appendSeq, match],
|
[appendSeq, match],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderItem = (index: number): React.ReactNode => {
|
||||||
|
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||||
|
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onPrependDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={filteredPrependSeq.map((x) => {
|
||||||
|
return x.name
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filteredPrependSeq.map((item) => {
|
||||||
|
return (
|
||||||
|
<ProxyItem
|
||||||
|
key={item.name}
|
||||||
|
type="prepend"
|
||||||
|
proxy={item}
|
||||||
|
onDelete={() => {
|
||||||
|
setPrependSeq(
|
||||||
|
prependSeq.filter((v) => v.name !== item.name),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
} else if (index < filteredProxyList.length + shift) {
|
||||||
|
const newIndex = index - shift
|
||||||
|
return (
|
||||||
|
<ProxyItem
|
||||||
|
key={filteredProxyList[newIndex].name}
|
||||||
|
type={
|
||||||
|
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||||
|
? 'delete'
|
||||||
|
: 'original'
|
||||||
|
}
|
||||||
|
proxy={filteredProxyList[newIndex]}
|
||||||
|
onDelete={() => {
|
||||||
|
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||||
|
setDeleteSeq(
|
||||||
|
deleteSeq.filter((v) => v !== filteredProxyList[newIndex].name),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setDeleteSeq((prev) => [
|
||||||
|
...prev,
|
||||||
|
filteredProxyList[newIndex].name,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onAppendDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={filteredAppendSeq.map((x) => {
|
||||||
|
return x.name
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filteredAppendSeq.map((item) => {
|
||||||
|
return (
|
||||||
|
<ProxyItem
|
||||||
|
key={item.name}
|
||||||
|
type="append"
|
||||||
|
proxy={item}
|
||||||
|
onDelete={() => {
|
||||||
|
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: { distance: 8 },
|
activationConstraint: { distance: 8 },
|
||||||
@ -366,109 +451,15 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
<Virtuoso
|
<VirtualList
|
||||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
count={
|
||||||
totalCount={
|
|
||||||
filteredProxyList.length +
|
filteredProxyList.length +
|
||||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
increaseViewportBy={256}
|
estimateSize={56}
|
||||||
itemContent={(index) => {
|
renderItem={renderItem}
|
||||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onPrependDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredPrependSeq.map((x) => {
|
|
||||||
return x.name
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{filteredPrependSeq.map((item) => {
|
|
||||||
return (
|
|
||||||
<ProxyItem
|
|
||||||
key={item.name}
|
|
||||||
type="prepend"
|
|
||||||
proxy={item}
|
|
||||||
onDelete={() => {
|
|
||||||
setPrependSeq(
|
|
||||||
prependSeq.filter(
|
|
||||||
(v) => v.name !== item.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
} else if (index < filteredProxyList.length + shift) {
|
|
||||||
const newIndex = index - shift
|
|
||||||
return (
|
|
||||||
<ProxyItem
|
|
||||||
key={filteredProxyList[newIndex].name}
|
|
||||||
type={
|
|
||||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
|
||||||
? 'delete'
|
|
||||||
: 'original'
|
|
||||||
}
|
|
||||||
proxy={filteredProxyList[newIndex]}
|
|
||||||
onDelete={() => {
|
|
||||||
if (
|
|
||||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
|
||||||
) {
|
|
||||||
setDeleteSeq(
|
|
||||||
deleteSeq.filter(
|
|
||||||
(v) => v !== filteredProxyList[newIndex].name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setDeleteSeq((prev) => [
|
|
||||||
...prev,
|
|
||||||
filteredProxyList[newIndex].name,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onAppendDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredAppendSeq.map((x) => {
|
|
||||||
return x.name
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{filteredAppendSeq.map((item) => {
|
|
||||||
return (
|
|
||||||
<ProxyItem
|
|
||||||
key={item.name}
|
|
||||||
type="append"
|
|
||||||
proxy={item}
|
|
||||||
onDelete={() => {
|
|
||||||
setAppendSeq(
|
|
||||||
appendSeq.filter(
|
|
||||||
(v) => v.name !== item.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -37,9 +37,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
|
||||||
|
|
||||||
import { BaseSearchBox, Switch } from '@/components/base'
|
import { BaseSearchBox, Switch, VirtualList } from '@/components/base'
|
||||||
import { RuleItem } from '@/components/profile/rule-item'
|
import { RuleItem } from '@/components/profile/rule-item'
|
||||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
@ -283,6 +282,87 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
[appendSeq, match],
|
[appendSeq, match],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderItem = (index: number): React.ReactNode => {
|
||||||
|
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||||
|
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onPrependDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={filteredPrependSeq.map((x) => {
|
||||||
|
return x
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filteredPrependSeq.map((item) => {
|
||||||
|
return (
|
||||||
|
<RuleItem
|
||||||
|
key={item}
|
||||||
|
type="prepend"
|
||||||
|
ruleRaw={item}
|
||||||
|
onDelete={() => {
|
||||||
|
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
} else if (index < filteredRuleList.length + shift) {
|
||||||
|
const newIndex = index - shift
|
||||||
|
return (
|
||||||
|
<RuleItem
|
||||||
|
key={filteredRuleList[newIndex]}
|
||||||
|
type={
|
||||||
|
deleteSeq.includes(filteredRuleList[newIndex])
|
||||||
|
? 'delete'
|
||||||
|
: 'original'
|
||||||
|
}
|
||||||
|
ruleRaw={filteredRuleList[newIndex]}
|
||||||
|
onDelete={() => {
|
||||||
|
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||||
|
setDeleteSeq(
|
||||||
|
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onAppendDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={filteredAppendSeq.map((x) => {
|
||||||
|
return x
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filteredAppendSeq.map((item) => {
|
||||||
|
return (
|
||||||
|
<RuleItem
|
||||||
|
key={item}
|
||||||
|
type="append"
|
||||||
|
ruleRaw={item}
|
||||||
|
onDelete={() => {
|
||||||
|
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: { distance: 8 },
|
activationConstraint: { distance: 8 },
|
||||||
@ -672,103 +752,15 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
<Virtuoso
|
<VirtualList
|
||||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
count={
|
||||||
totalCount={
|
|
||||||
filteredRuleList.length +
|
filteredRuleList.length +
|
||||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
increaseViewportBy={256}
|
estimateSize={56}
|
||||||
itemContent={(index) => {
|
renderItem={renderItem}
|
||||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onPrependDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredPrependSeq.map((x) => {
|
|
||||||
return x
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{filteredPrependSeq.map((item) => {
|
|
||||||
return (
|
|
||||||
<RuleItem
|
|
||||||
key={item}
|
|
||||||
type="prepend"
|
|
||||||
ruleRaw={item}
|
|
||||||
onDelete={() => {
|
|
||||||
setPrependSeq(
|
|
||||||
prependSeq.filter((v) => v !== item),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
} else if (index < filteredRuleList.length + shift) {
|
|
||||||
const newIndex = index - shift
|
|
||||||
return (
|
|
||||||
<RuleItem
|
|
||||||
key={filteredRuleList[newIndex]}
|
|
||||||
type={
|
|
||||||
deleteSeq.includes(filteredRuleList[newIndex])
|
|
||||||
? 'delete'
|
|
||||||
: 'original'
|
|
||||||
}
|
|
||||||
ruleRaw={filteredRuleList[newIndex]}
|
|
||||||
onDelete={() => {
|
|
||||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
|
||||||
setDeleteSeq(
|
|
||||||
deleteSeq.filter(
|
|
||||||
(v) => v !== filteredRuleList[newIndex],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setDeleteSeq((prev) => [
|
|
||||||
...prev,
|
|
||||||
filteredRuleList[newIndex],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onAppendDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={filteredAppendSeq.map((x) => {
|
|
||||||
return x
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{filteredAppendSeq.map((item) => {
|
|
||||||
return (
|
|
||||||
<RuleItem
|
|
||||||
key={item}
|
|
||||||
type="append"
|
|
||||||
ruleRaw={item}
|
|
||||||
onDelete={() => {
|
|
||||||
setAppendSeq(
|
|
||||||
appendSeq.filter((v) => v !== item),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import {
|
|||||||
Snackbar,
|
Snackbar,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
|
||||||
import { delayGroup, healthcheckProxyProvider } from 'tauri-plugin-mihomo-api'
|
import { delayGroup, healthcheckProxyProvider } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { BaseEmpty } from '@/components/base'
|
import { BaseEmpty } from '@/components/base'
|
||||||
@ -46,8 +46,6 @@ interface ProxyChainItem {
|
|||||||
delay?: number
|
delay?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtuosoFooter = () => <div style={{ height: '8px' }} />
|
|
||||||
|
|
||||||
export const ProxyGroups = (props: Props) => {
|
export const ProxyGroups = (props: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mode, isChainMode = false, chainConfigData } = props
|
const { mode, isChainMode = false, chainConfigData } = props
|
||||||
@ -129,10 +127,17 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
const timeout = verge?.default_latency_timeout || 10000
|
const timeout = verge?.default_latency_timeout || 10000
|
||||||
|
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
const parentRef = useRef<HTMLDivElement>(null)
|
||||||
const scrollPositionRef = useRef<Record<string, number>>({})
|
const scrollPositionRef = useRef<Record<string, number>>({})
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
const scrollerRef = useRef<Element | null>(null)
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: renderList.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 56,
|
||||||
|
overscan: 15,
|
||||||
|
getItemKey: (index) => renderList[index]?.key ?? index,
|
||||||
|
})
|
||||||
|
|
||||||
// 从 localStorage 恢复滚动位置
|
// 从 localStorage 恢复滚动位置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -149,10 +154,9 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
if (savedPosition !== undefined) {
|
if (savedPosition !== undefined) {
|
||||||
restoreTimer = setTimeout(() => {
|
restoreTimer = setTimeout(() => {
|
||||||
virtuosoRef.current?.scrollTo({
|
if (parentRef.current) {
|
||||||
top: savedPosition,
|
parentRef.current.scrollTop = savedPosition
|
||||||
behavior: 'auto',
|
}
|
||||||
})
|
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,7 +202,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
// 添加和清理滚动事件监听器
|
// 添加和清理滚动事件监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = scrollerRef.current
|
const node = parentRef.current
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
|
||||||
const listener = handleScroll as EventListener
|
const listener = handleScroll as EventListener
|
||||||
@ -213,7 +217,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
// 滚动到顶部
|
// 滚动到顶部
|
||||||
const scrollToTop = useCallback(() => {
|
const scrollToTop = useCallback(() => {
|
||||||
virtuosoRef.current?.scrollTo?.({
|
parentRef.current?.scrollTo?.({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
@ -362,11 +366,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
virtuosoRef.current?.scrollToIndex?.({
|
virtualizer.scrollToIndex(index, { align: 'center', behavior: 'smooth' })
|
||||||
index,
|
|
||||||
align: 'center',
|
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,14 +378,10 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
virtuosoRef.current?.scrollToIndex?.({
|
virtualizer.scrollToIndex(index, { align: 'start', behavior: 'smooth' })
|
||||||
index,
|
|
||||||
align: 'start',
|
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[renderList],
|
[renderList, virtualizer],
|
||||||
)
|
)
|
||||||
|
|
||||||
const proxyGroupNames = useMemo(() => {
|
const proxyGroupNames = useMemo(() => {
|
||||||
@ -475,39 +471,49 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Virtuoso
|
<div
|
||||||
ref={virtuosoRef}
|
ref={parentRef}
|
||||||
style={{
|
style={{
|
||||||
height:
|
height:
|
||||||
mode === 'rule' && proxyGroups.length > 0
|
mode === 'rule' && proxyGroups.length > 0
|
||||||
? 'calc(100% - 80px)' // 只有标题的高度
|
? 'calc(100% - 80px)' // 只有标题的高度
|
||||||
: 'calc(100% - 14px)',
|
: 'calc(100% - 14px)',
|
||||||
|
overflow: 'auto',
|
||||||
}}
|
}}
|
||||||
totalCount={renderList.length}
|
>
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
<div
|
||||||
overscan={150}
|
style={{
|
||||||
defaultItemHeight={56}
|
height: virtualizer.getTotalSize(),
|
||||||
scrollerRef={(ref) => {
|
position: 'relative',
|
||||||
scrollerRef.current = ref as Element
|
}}
|
||||||
}}
|
>
|
||||||
components={{
|
{virtualizer.getVirtualItems().map((virtualItem) => (
|
||||||
Footer: VirtuosoFooter,
|
<div
|
||||||
}}
|
key={virtualItem.key}
|
||||||
initialScrollTop={scrollPositionRef.current[mode]}
|
data-index={virtualItem.index}
|
||||||
computeItemKey={(index) => renderList[index].key}
|
ref={virtualizer.measureElement}
|
||||||
itemContent={(index) => (
|
style={{
|
||||||
<ProxyRender
|
position: 'absolute',
|
||||||
key={renderList[index].key}
|
top: 0,
|
||||||
item={renderList[index]}
|
left: 0,
|
||||||
indent={mode === 'rule' || mode === 'script'}
|
width: '100%',
|
||||||
onLocation={handleLocation}
|
transform: `translateY(${virtualItem.start}px)`,
|
||||||
onCheckAll={handleCheckAll}
|
}}
|
||||||
onHeadState={onHeadState}
|
>
|
||||||
onChangeProxy={handleChangeProxy}
|
<ProxyRender
|
||||||
isChainMode={isChainMode}
|
item={renderList[virtualItem.index]}
|
||||||
/>
|
indent={mode === 'rule' || mode === 'script'}
|
||||||
)}
|
onLocation={handleLocation}
|
||||||
/>
|
onCheckAll={handleCheckAll}
|
||||||
|
onHeadState={onHeadState}
|
||||||
|
onChangeProxy={handleChangeProxy}
|
||||||
|
isChainMode={isChainMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ height: 8 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -603,34 +609,42 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Virtuoso
|
<div
|
||||||
ref={virtuosoRef}
|
ref={parentRef}
|
||||||
style={{ height: 'calc(100% - 14px)' }}
|
style={{ height: 'calc(100% - 14px)', overflow: 'auto' }}
|
||||||
totalCount={renderList.length}
|
>
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
<div
|
||||||
overscan={150}
|
style={{
|
||||||
defaultItemHeight={56}
|
height: virtualizer.getTotalSize(),
|
||||||
scrollerRef={(ref) => {
|
position: 'relative',
|
||||||
scrollerRef.current = ref as Element
|
}}
|
||||||
}}
|
>
|
||||||
components={{
|
{virtualizer.getVirtualItems().map((virtualItem) => (
|
||||||
Footer: VirtuosoFooter,
|
<div
|
||||||
}}
|
key={virtualItem.key}
|
||||||
// 添加平滑滚动设置
|
data-index={virtualItem.index}
|
||||||
initialScrollTop={scrollPositionRef.current[mode]}
|
ref={virtualizer.measureElement}
|
||||||
computeItemKey={(index) => renderList[index].key}
|
style={{
|
||||||
itemContent={(index) => (
|
position: 'absolute',
|
||||||
<ProxyRender
|
top: 0,
|
||||||
key={renderList[index].key}
|
left: 0,
|
||||||
item={renderList[index]}
|
width: '100%',
|
||||||
indent={mode === 'rule' || mode === 'script'}
|
transform: `translateY(${virtualItem.start}px)`,
|
||||||
onLocation={handleLocation}
|
}}
|
||||||
onCheckAll={handleCheckAll}
|
>
|
||||||
onHeadState={onHeadState}
|
<ProxyRender
|
||||||
onChangeProxy={handleChangeProxy}
|
item={renderList[virtualItem.index]}
|
||||||
/>
|
indent={mode === 'rule' || mode === 'script'}
|
||||||
)}
|
onLocation={handleLocation}
|
||||||
/>
|
onCheckAll={handleCheckAll}
|
||||||
|
onHeadState={onHeadState}
|
||||||
|
onChangeProxy={handleChangeProxy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ height: 8 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -15,10 +15,10 @@ import { useLockFn } from 'ahooks'
|
|||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import { useImperativeHandle, useState } from 'react'
|
import { useImperativeHandle, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { mutate } from 'swr'
|
|
||||||
import { closeAllConnections, upgradeCore } from 'tauri-plugin-mihomo-api'
|
import { closeAllConnections, upgradeCore } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { BaseDialog, DialogRef } from '@/components/base'
|
import { BaseDialog, DialogRef } from '@/components/base'
|
||||||
|
import { useClash, useClashInfo } from '@/hooks/use-clash'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { changeClashCore, restartCore } from '@/services/cmds'
|
import { changeClashCore, restartCore } from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
@ -40,6 +40,8 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { verge, mutateVerge } = useVerge()
|
const { verge, mutateVerge } = useVerge()
|
||||||
|
const { mutateVersion } = useClash()
|
||||||
|
const { invalidateClashConfig } = useClashInfo()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [upgrading, setUpgrading] = useState(false)
|
const [upgrading, setUpgrading] = useState(false)
|
||||||
@ -69,8 +71,8 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
mutateVerge()
|
mutateVerge()
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
mutate('getClashConfig')
|
invalidateClashConfig()
|
||||||
mutate('getVersion')
|
mutateVersion()
|
||||||
setChangingCore(null)
|
setChangingCore(null)
|
||||||
}, 500)
|
}, 500)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { mutate } from 'swr'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BaseDialog,
|
BaseDialog,
|
||||||
@ -110,7 +109,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const [hostOptions, setHostOptions] = useState<string[]>([])
|
const [hostOptions, setHostOptions] = useState<string[]>([])
|
||||||
|
|
||||||
const { clashConfig } = useAppData()
|
const { clashConfig } = useAppData()
|
||||||
const { indicator: isProxyReallyEnabled } = useSystemProxyState()
|
const { indicator: isProxyReallyEnabled, invalidateProxyState } =
|
||||||
|
useSystemProxyState()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
enable_system_proxy: enabled,
|
enable_system_proxy: enabled,
|
||||||
@ -166,10 +166,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
await patchVergeConfig({ enable_system_proxy: false })
|
await patchVergeConfig({ enable_system_proxy: false })
|
||||||
await sleep(200)
|
await sleep(200)
|
||||||
await patchVergeConfig({ enable_system_proxy: true })
|
await patchVergeConfig({ enable_system_proxy: true })
|
||||||
await Promise.all([
|
await invalidateProxyState()
|
||||||
mutate('getSystemProxy'),
|
|
||||||
mutate('getAutotemProxy'),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err)
|
showNotice.error(err)
|
||||||
@ -177,7 +174,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateProxy()
|
updateProxy()
|
||||||
}, [clashConfig?.mixedPort, value.pac])
|
}, [clashConfig?.mixedPort, value.pac, invalidateProxyState])
|
||||||
|
|
||||||
const { systemProxyAddress } = useAppData()
|
const { systemProxyAddress } = useAppData()
|
||||||
|
|
||||||
@ -410,10 +407,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await invalidateProxyState()
|
||||||
mutate('getSystemProxy'),
|
|
||||||
mutate('getAutotemProxy'),
|
|
||||||
])
|
|
||||||
|
|
||||||
// 如果需要重置代理且代理当前启用
|
// 如果需要重置代理且代理当前启用
|
||||||
if (needResetProxy && enabled) {
|
if (needResetProxy && enabled) {
|
||||||
@ -430,10 +424,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
await patchVergeConfig({ enable_system_proxy: false })
|
await patchVergeConfig({ enable_system_proxy: false })
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||||
await patchVergeConfig({ enable_system_proxy: true })
|
await patchVergeConfig({ enable_system_proxy: true })
|
||||||
await Promise.all([
|
await invalidateProxyState()
|
||||||
mutate('getSystemProxy'),
|
|
||||||
mutate('getAutotemProxy'),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { mutate } from 'swr'
|
|
||||||
|
|
||||||
import { DialogRef, Switch, TooltipIcon } from '@/components/base'
|
import { DialogRef, Switch, TooltipIcon } from '@/components/base'
|
||||||
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
|
||||||
@ -62,7 +61,6 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
// 先触发UI更新立即看到反馈
|
// 先触发UI更新立即看到反馈
|
||||||
onChangeData({ enable_auto_launch: e })
|
onChangeData({ enable_auto_launch: e })
|
||||||
await patchVerge({ enable_auto_launch: e })
|
await patchVerge({ enable_auto_launch: e })
|
||||||
await mutate('getAutoLaunchStatus')
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果出错,恢复原始状态
|
// 如果出错,恢复原始状态
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import useSWR, { mutate } from 'swr'
|
|
||||||
import { getVersion } from 'tauri-plugin-mihomo-api'
|
import { getVersion } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -7,6 +7,12 @@ import {
|
|||||||
getRuntimeConfig,
|
getRuntimeConfig,
|
||||||
patchClashConfig,
|
patchClashConfig,
|
||||||
} from '@/services/cmds'
|
} from '@/services/cmds'
|
||||||
|
import { queryClient } from '@/services/query-client'
|
||||||
|
|
||||||
|
type MutateClashUpdater =
|
||||||
|
| ((old: IConfigData | undefined) => IConfigData | undefined)
|
||||||
|
| IConfigData
|
||||||
|
| undefined
|
||||||
|
|
||||||
const PORT_KEYS = [
|
const PORT_KEYS = [
|
||||||
'port',
|
'port',
|
||||||
@ -38,7 +44,7 @@ const validatePortRange = (port: number) => {
|
|||||||
if (port < 1000) {
|
if (port < 1000) {
|
||||||
throw new Error('The port should not < 1000')
|
throw new Error('The port should not < 1000')
|
||||||
}
|
}
|
||||||
if (port > 65536) {
|
if (port > 65535) {
|
||||||
throw new Error('The port should not > 65536')
|
throw new Error('The port should not > 65536')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,16 +58,35 @@ const validatePorts = (patch: ClashInfoPatch) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useRuntimeConfig = (shouldFetch: boolean = true) => {
|
export const useRuntimeConfig = (shouldFetch: boolean = true) => {
|
||||||
return useSWR(shouldFetch ? 'getRuntimeConfig' : null, getRuntimeConfig)
|
return useQuery({
|
||||||
|
queryKey: ['getRuntimeConfig'],
|
||||||
|
queryFn: getRuntimeConfig,
|
||||||
|
enabled: shouldFetch,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useClash = () => {
|
export const useClash = () => {
|
||||||
const { data: clash, mutate: mutateClash } = useRuntimeConfig()
|
const { data: clash, refetch } = useRuntimeConfig()
|
||||||
|
|
||||||
const { data: versionData, mutate: mutateVersion } = useSWR(
|
const { data: versionData, refetch: mutateVersion } = useQuery({
|
||||||
'getVersion',
|
queryKey: ['getVersion'],
|
||||||
getVersion,
|
queryFn: getVersion,
|
||||||
)
|
})
|
||||||
|
|
||||||
|
const mutateClash = (updater?: MutateClashUpdater, revalidate?: boolean) => {
|
||||||
|
if (updater === undefined) {
|
||||||
|
return refetch()
|
||||||
|
}
|
||||||
|
const next =
|
||||||
|
typeof updater === 'function'
|
||||||
|
? updater(queryClient.getQueryData<IConfigData>(['getRuntimeConfig']))
|
||||||
|
: updater
|
||||||
|
queryClient.setQueryData(['getRuntimeConfig'], next)
|
||||||
|
if (revalidate !== false) {
|
||||||
|
return refetch()
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
const patchClash = useLockFn(async (patch: Partial<IConfigData>) => {
|
const patchClash = useLockFn(async (patch: Partial<IConfigData>) => {
|
||||||
await patchClashConfig(patch)
|
await patchClashConfig(patch)
|
||||||
@ -82,10 +107,10 @@ export const useClash = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useClashInfo = () => {
|
export const useClashInfo = () => {
|
||||||
const { data: clashInfo, mutate: mutateInfo } = useSWR(
|
const { data: clashInfo, refetch: mutateInfo } = useQuery({
|
||||||
'getClashInfo',
|
queryKey: ['getClashInfo'],
|
||||||
getClashInfo,
|
queryFn: getClashInfo,
|
||||||
)
|
})
|
||||||
|
|
||||||
const patchInfo = useLockFn(async (patch: ClashInfoPatch) => {
|
const patchInfo = useLockFn(async (patch: ClashInfoPatch) => {
|
||||||
if (!hasClashInfoPayload(patch)) return
|
if (!hasClashInfoPayload(patch)) return
|
||||||
@ -94,12 +119,16 @@ export const useClashInfo = () => {
|
|||||||
|
|
||||||
await patchClashConfig(patch)
|
await patchClashConfig(patch)
|
||||||
mutateInfo()
|
mutateInfo()
|
||||||
mutate('getClashConfig')
|
queryClient.invalidateQueries({ queryKey: ['getClashConfig'] })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const invalidateClashConfig = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getClashConfig'] })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clashInfo,
|
clashInfo,
|
||||||
mutateInfo,
|
mutateInfo,
|
||||||
patchInfo,
|
patchInfo,
|
||||||
|
invalidateClashConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { mutate } from 'swr'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
import { MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useMihomoWsSubscription } from './use-mihomo-ws-subscription'
|
import { useMihomoWsSubscription } from './use-mihomo-ws-subscription'
|
||||||
@ -33,7 +33,6 @@ const mergeConnectionSnapshot = (
|
|||||||
const nextConnections = payload.connections ?? []
|
const nextConnections = payload.connections ?? []
|
||||||
const previousActive = previous.activeConnections ?? []
|
const previousActive = previous.activeConnections ?? []
|
||||||
const nextById = new Map(nextConnections.map((conn) => [conn.id, conn]))
|
const nextById = new Map(nextConnections.map((conn) => [conn.id, conn]))
|
||||||
const newIds = new Set(nextConnections.map((conn) => conn.id))
|
|
||||||
|
|
||||||
// Keep surviving connections in their previous relative order to reduce row reshuffle,
|
// Keep surviving connections in their previous relative order to reduce row reshuffle,
|
||||||
// but constrain the array to the incoming snapshot length.
|
// but constrain the array to the incoming snapshot length.
|
||||||
@ -60,10 +59,11 @@ const mergeConnectionSnapshot = (
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const activeConnections = [...carried, ...newcomers]
|
const activeConnections = [...carried, ...newcomers]
|
||||||
|
const activeIds = new Set(activeConnections.map((conn) => conn.id))
|
||||||
|
|
||||||
const closedConnections = trimClosedConnections([
|
const closedConnections = trimClosedConnections([
|
||||||
...(previous.closedConnections ?? []),
|
...(previous.closedConnections ?? []),
|
||||||
...previousActive.filter((conn) => !newIds.has(conn.id)),
|
...previousActive.filter((conn) => !activeIds.has(conn.id)),
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -75,6 +75,7 @@ const mergeConnectionSnapshot = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useConnectionData = () => {
|
export const useConnectionData = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { response, refresh, subscriptionCacheKey } =
|
const { response, refresh, subscriptionCacheKey } =
|
||||||
useMihomoWsSubscription<ConnectionMonitorData>({
|
useMihomoWsSubscription<ConnectionMonitorData>({
|
||||||
storageKey: 'mihomo_connection_date',
|
storageKey: 'mihomo_connection_date',
|
||||||
@ -99,7 +100,7 @@ export const useConnectionData = () => {
|
|||||||
|
|
||||||
const clearClosedConnections = () => {
|
const clearClosedConnections = () => {
|
||||||
if (!subscriptionCacheKey) return
|
if (!subscriptionCacheKey) return
|
||||||
mutate(subscriptionCacheKey, {
|
queryClient.setQueryData<ConnectionMonitorData>([subscriptionCacheKey], {
|
||||||
uploadTotal: response.data?.uploadTotal ?? 0,
|
uploadTotal: response.data?.uploadTotal ?? 0,
|
||||||
downloadTotal: response.data?.downloadTotal ?? 0,
|
downloadTotal: response.data?.downloadTotal ?? 0,
|
||||||
activeConnections: response.data?.activeConnections ?? [],
|
activeConnections: response.data?.activeConnections ?? [],
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import useSWR from 'swr'
|
|
||||||
|
|
||||||
import { downloadIconCache } from '@/services/cmds'
|
import { downloadIconCache } from '@/services/cmds'
|
||||||
import { SWR_DEFAULTS } from '@/services/config'
|
|
||||||
|
|
||||||
export interface UseIconCacheOptions {
|
export interface UseIconCacheOptions {
|
||||||
icon?: string | null
|
icon?: string | null
|
||||||
@ -24,17 +23,13 @@ export const useIconCache = ({
|
|||||||
const iconValue = icon?.trim() ?? ''
|
const iconValue = icon?.trim() ?? ''
|
||||||
const cacheKeyValue = cacheKey?.trim() ?? ''
|
const cacheKeyValue = cacheKey?.trim() ?? ''
|
||||||
|
|
||||||
const swrKey = useMemo(() => {
|
const isEnabled = useMemo(() => {
|
||||||
if (!enabled || !iconValue.startsWith('http') || cacheKeyValue === '') {
|
return enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['icon-cache', iconValue, cacheKeyValue] as const
|
|
||||||
}, [enabled, iconValue, cacheKeyValue])
|
}, [enabled, iconValue, cacheKeyValue])
|
||||||
|
|
||||||
const { data } = useSWR(
|
const { data } = useQuery({
|
||||||
swrKey,
|
queryKey: ['icon-cache', iconValue, cacheKeyValue],
|
||||||
async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const fileName = `${cacheKeyValue}-${getFileNameFromUrl(iconValue)}`
|
const fileName = `${cacheKeyValue}-${getFileNameFromUrl(iconValue)}`
|
||||||
const iconPath = await downloadIconCache(iconValue, fileName)
|
const iconPath = await downloadIconCache(iconValue, fileName)
|
||||||
@ -43,10 +38,15 @@ export const useIconCache = ({
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SWR_DEFAULTS,
|
enabled: isEnabled,
|
||||||
)
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
staleTime: Infinity,
|
||||||
|
gcTime: Infinity,
|
||||||
|
retry: 2,
|
||||||
|
})
|
||||||
|
|
||||||
if (!swrKey) {
|
if (!isEnabled) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { mutate } from 'swr'
|
|
||||||
import { MihomoWebSocket, type LogLevel } from 'tauri-plugin-mihomo-api'
|
import { MihomoWebSocket, type LogLevel } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { getClashLogs } from '@/services/cmds'
|
import { getClashLogs } from '@/services/cmds'
|
||||||
@ -39,6 +39,7 @@ const appendLogs = (
|
|||||||
): ILogItem[] => clampLogs([...(current ?? []), ...incoming])
|
): ILogItem[] => clampLogs([...(current ?? []), ...incoming])
|
||||||
|
|
||||||
export const useLogData = () => {
|
export const useLogData = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [clashLog] = useClashLog()
|
const [clashLog] = useClashLog()
|
||||||
const enableLog = clashLog.enable
|
const enableLog = clashLog.enable
|
||||||
const logLevel = clashLog.logLevel
|
const logLevel = clashLog.logLevel
|
||||||
@ -50,7 +51,6 @@ export const useLogData = () => {
|
|||||||
storageKey: 'mihomo_logs_date',
|
storageKey: 'mihomo_logs_date',
|
||||||
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
|
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
|
||||||
fallbackData: [],
|
fallbackData: [],
|
||||||
keepPreviousData: true,
|
|
||||||
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
||||||
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
||||||
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@ -91,6 +91,9 @@ export const useLogData = () => {
|
|||||||
}
|
}
|
||||||
parsed.time = dayjs().format('MM-DD HH:mm:ss')
|
parsed.time = dayjs().format('MM-DD HH:mm:ss')
|
||||||
buffer.push(parsed)
|
buffer.push(parsed)
|
||||||
|
if (buffer.length > MAX_LOG_NUM) {
|
||||||
|
buffer.splice(0, buffer.length - MAX_LOG_NUM)
|
||||||
|
}
|
||||||
if (!flushTimer) {
|
if (!flushTimer) {
|
||||||
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
||||||
}
|
}
|
||||||
@ -133,7 +136,7 @@ export const useLogData = () => {
|
|||||||
const refreshGetClashLog = (clear = false) => {
|
const refreshGetClashLog = (clear = false) => {
|
||||||
if (clear) {
|
if (clear) {
|
||||||
if (subscriptionCacheKey) {
|
if (subscriptionCacheKey) {
|
||||||
mutate(subscriptionCacheKey, [])
|
queryClient.setQueryData<ILogItem[]>([subscriptionCacheKey], [])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
refresh()
|
refresh()
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import { mutate, type MutatorCallback } from 'swr'
|
|
||||||
import useSWRSubscription from 'swr/subscription'
|
|
||||||
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
export const RECONNECT_DELAY_MS = 1000
|
export const RECONNECT_DELAY_MS = 1000
|
||||||
|
|
||||||
type NextFn<T> = (error?: any, data?: T | MutatorCallback<T>) => void
|
/**
|
||||||
|
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
|
||||||
|
* functional updater `(current?: T) => T`. The functional form is resolved
|
||||||
|
* against the current cache entry before calling `queryClient.setQueryData`.
|
||||||
|
*/
|
||||||
|
type NextFn<T> = (
|
||||||
|
error?: any,
|
||||||
|
data?: T | ((current?: T) => T | undefined),
|
||||||
|
) => void
|
||||||
|
|
||||||
interface HandlerContext<T> {
|
interface HandlerContext<T> {
|
||||||
next: NextFn<T>
|
next: NextFn<T>
|
||||||
@ -25,7 +32,6 @@ interface UseMihomoWsSubscriptionOptions<T> {
|
|||||||
buildSubscriptKey: (date: number) => string | null
|
buildSubscriptKey: (date: number) => string | null
|
||||||
fallbackData: T
|
fallbackData: T
|
||||||
connect: () => Promise<MihomoWebSocket>
|
connect: () => Promise<MihomoWebSocket>
|
||||||
keepPreviousData?: boolean
|
|
||||||
/**
|
/**
|
||||||
* When > 0, coalesce rapid WebSocket messages by wrapping the `next`
|
* When > 0, coalesce rapid WebSocket messages by wrapping the `next`
|
||||||
* function passed to `setupHandlers`. Only the most recent value is
|
* function passed to `setupHandlers`. Only the most recent value is
|
||||||
@ -46,7 +52,6 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
buildSubscriptKey,
|
buildSubscriptKey,
|
||||||
fallbackData,
|
fallbackData,
|
||||||
connect,
|
connect,
|
||||||
keepPreviousData = true,
|
|
||||||
throttleMs,
|
throttleMs,
|
||||||
setupHandlers,
|
setupHandlers,
|
||||||
} = options
|
} = options
|
||||||
@ -56,148 +61,185 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
const subscriptKey = buildSubscriptKey(date)
|
const subscriptKey = buildSubscriptKey(date)
|
||||||
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null
|
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const wsRef = useRef<MihomoWebSocket | null>(null)
|
const wsRef = useRef<MihomoWebSocket | null>(null)
|
||||||
const wsFirstConnectionRef = useRef<boolean>(true)
|
const wsFirstConnectionRef = useRef<boolean>(true)
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
const response = useSWRSubscription<T, any, string | null>(
|
const resolveNextData = useCallback(
|
||||||
subscriptKey,
|
(
|
||||||
(_key, { next }) => {
|
data: T | ((current?: T) => T | undefined) | undefined,
|
||||||
let isMounted = true
|
cacheKey: string,
|
||||||
|
): T => {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
const updater = data as (current?: T) => T | undefined
|
||||||
|
const current = queryClient.getQueryData<T>([cacheKey])
|
||||||
|
return updater(current) ?? fallbackData
|
||||||
|
}
|
||||||
|
return data ?? fallbackData
|
||||||
|
},
|
||||||
|
[queryClient, fallbackData],
|
||||||
|
)
|
||||||
|
|
||||||
const clearReconnectTimer = () => {
|
const response = useQuery<T>({
|
||||||
if (timeoutRef.current) {
|
queryKey: subscriptionCacheKey
|
||||||
clearTimeout(timeoutRef.current)
|
? [subscriptionCacheKey]
|
||||||
timeoutRef.current = null
|
: ['$sub$__disabled__'],
|
||||||
|
queryFn: () =>
|
||||||
|
queryClient.getQueryData<T>([subscriptionCacheKey!]) ?? fallbackData,
|
||||||
|
initialData: () =>
|
||||||
|
queryClient.getQueryData<T>([
|
||||||
|
subscriptionCacheKey ?? '$sub$__disabled__',
|
||||||
|
]) ?? fallbackData,
|
||||||
|
staleTime: Infinity,
|
||||||
|
gcTime: Infinity,
|
||||||
|
enabled: subscriptionCacheKey !== null,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subscriptionCacheKey) return
|
||||||
|
|
||||||
|
let isMounted = true
|
||||||
|
|
||||||
|
const clearReconnectTimer = () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSocket = async () => {
|
||||||
|
if (wsRef.current) {
|
||||||
|
await wsRef.current.close()
|
||||||
|
wsRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleReconnect = async () => {
|
||||||
|
if (!isMounted) return
|
||||||
|
clearReconnectTimer()
|
||||||
|
await closeSocket()
|
||||||
|
if (!isMounted) return
|
||||||
|
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
let throttleCleanup: (() => void) | undefined
|
||||||
|
let wrappedNext: NextFn<T>
|
||||||
|
|
||||||
|
const baseNext: NextFn<T> = (error, data) => {
|
||||||
|
if (error !== undefined && error !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data === undefined) return
|
||||||
|
const resolved = resolveNextData(data, subscriptionCacheKey)
|
||||||
|
queryClient.setQueryData<T>([subscriptionCacheKey], resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (throttleMs && throttleMs > 0) {
|
||||||
|
let pendingData: T | ((current?: T) => T | undefined) | undefined
|
||||||
|
let hasPending = false
|
||||||
|
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
timerId = null
|
||||||
|
if (hasPending) {
|
||||||
|
const data = pendingData
|
||||||
|
pendingData = undefined
|
||||||
|
hasPending = false
|
||||||
|
baseNext(undefined, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSocket = async () => {
|
wrappedNext = (
|
||||||
if (wsRef.current) {
|
error?: any,
|
||||||
await wsRef.current.close()
|
data?: T | ((current?: T) => T | undefined),
|
||||||
wsRef.current = null
|
) => {
|
||||||
|
if (error !== undefined && error !== null) {
|
||||||
|
baseNext(error, data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!timerId) {
|
||||||
|
baseNext(undefined, data)
|
||||||
|
timerId = setTimeout(flush, throttleMs)
|
||||||
|
} else {
|
||||||
|
pendingData = data
|
||||||
|
hasPending = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduleReconnect = async () => {
|
throttleCleanup = () => {
|
||||||
if (!isMounted) return
|
if (timerId) {
|
||||||
clearReconnectTimer()
|
clearTimeout(timerId)
|
||||||
await closeSocket()
|
|
||||||
if (!isMounted) return
|
|
||||||
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
let throttleCleanup: (() => void) | undefined
|
|
||||||
let wrappedNext: NextFn<T> = next
|
|
||||||
|
|
||||||
if (throttleMs && throttleMs > 0) {
|
|
||||||
let pendingData: T | MutatorCallback<T> | undefined
|
|
||||||
let hasPending = false
|
|
||||||
let timerId: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const flush = () => {
|
|
||||||
timerId = null
|
timerId = null
|
||||||
if (hasPending) {
|
|
||||||
const data = pendingData
|
|
||||||
pendingData = undefined
|
|
||||||
hasPending = false
|
|
||||||
next(undefined, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wrappedNext = (error?: any, data?: T | MutatorCallback<T>) => {
|
|
||||||
if (error !== undefined && error !== null) {
|
|
||||||
next(error, data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!timerId) {
|
|
||||||
next(undefined, data)
|
|
||||||
timerId = setTimeout(flush, throttleMs)
|
|
||||||
} else {
|
|
||||||
pendingData = data
|
|
||||||
hasPending = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throttleCleanup = () => {
|
|
||||||
if (timerId) {
|
|
||||||
clearTimeout(timerId)
|
|
||||||
timerId = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
wrappedNext = baseNext
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleMessage: handleTextMessage,
|
handleMessage: handleTextMessage,
|
||||||
onConnected,
|
onConnected,
|
||||||
cleanup,
|
cleanup,
|
||||||
} = setupHandlers({
|
} = setupHandlers({
|
||||||
next: wrappedNext,
|
next: wrappedNext,
|
||||||
scheduleReconnect,
|
scheduleReconnect,
|
||||||
isMounted: () => isMounted,
|
isMounted: () => isMounted,
|
||||||
})
|
})
|
||||||
|
|
||||||
const cleanupAll = () => {
|
const cleanupAll = () => {
|
||||||
|
clearReconnectTimer()
|
||||||
|
throttleCleanup?.()
|
||||||
|
cleanup?.()
|
||||||
|
void closeSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMessage = (msg: Message) => {
|
||||||
|
if (msg.type !== 'Text') return
|
||||||
|
handleTextMessage(msg.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWs() {
|
||||||
|
try {
|
||||||
|
const ws_ = await connect()
|
||||||
|
if (!isMounted) {
|
||||||
|
await ws_.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wsRef.current = ws_
|
||||||
clearReconnectTimer()
|
clearReconnectTimer()
|
||||||
throttleCleanup?.()
|
|
||||||
cleanup?.()
|
|
||||||
void closeSocket()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMessage = (msg: Message) => {
|
if (onConnected) {
|
||||||
if (msg.type !== 'Text') return
|
await onConnected(ws_)
|
||||||
handleTextMessage(msg.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectWs() {
|
|
||||||
try {
|
|
||||||
const ws_ = await connect()
|
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
await ws_.close()
|
await ws_.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wsRef.current = ws_
|
ws_.addListener(handleMessage)
|
||||||
clearReconnectTimer()
|
} catch (ignoreError) {
|
||||||
|
if (!wsRef.current && isMounted) {
|
||||||
if (onConnected) {
|
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
|
||||||
await onConnected(ws_)
|
|
||||||
if (!isMounted) {
|
|
||||||
await ws_.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws_.addListener(handleMessage)
|
|
||||||
} catch (ignoreError) {
|
|
||||||
if (!wsRef.current && isMounted) {
|
|
||||||
timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wsFirstConnectionRef.current || !wsRef.current) {
|
|
||||||
wsFirstConnectionRef.current = false
|
|
||||||
cleanupAll()
|
|
||||||
void connectWs()
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false
|
|
||||||
wsFirstConnectionRef.current = true
|
|
||||||
cleanupAll()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fallbackData,
|
|
||||||
keepPreviousData,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (subscriptionCacheKey) {
|
|
||||||
mutate(subscriptionCacheKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wsFirstConnectionRef.current || !wsRef.current) {
|
||||||
|
wsFirstConnectionRef.current = false
|
||||||
|
cleanupAll()
|
||||||
|
void connectWs()
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false
|
||||||
|
wsFirstConnectionRef.current = true
|
||||||
|
cleanupAll()
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
||||||
}, [subscriptionCacheKey])
|
}, [subscriptionCacheKey])
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
import useSWR from 'swr'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { getNetworkInterfacesInfo } from '@/services/cmds'
|
import { getNetworkInterfacesInfo } from '@/services/cmds'
|
||||||
|
|
||||||
export const useNetworkInterfaces = () => {
|
export const useNetworkInterfaces = () => {
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
const {
|
||||||
'getNetworkInterfacesInfo',
|
data,
|
||||||
getNetworkInterfacesInfo,
|
error,
|
||||||
{
|
isLoading,
|
||||||
revalidateOnFocus: false,
|
refetch: mutate,
|
||||||
revalidateOnReconnect: false,
|
} = useQuery({
|
||||||
fallbackData: [],
|
queryKey: ['getNetworkInterfacesInfo'],
|
||||||
},
|
queryFn: getNetworkInterfacesInfo,
|
||||||
)
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
initialData: [],
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
networkInterfaces: data || [],
|
networkInterfaces: data || [],
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import useSWR, { mutate } from 'swr'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { selectNodeForGroup } from 'tauri-plugin-mihomo-api'
|
import { selectNodeForGroup } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -7,32 +7,37 @@ import {
|
|||||||
patchProfile,
|
patchProfile,
|
||||||
patchProfilesConfig,
|
patchProfilesConfig,
|
||||||
} from '@/services/cmds'
|
} from '@/services/cmds'
|
||||||
|
import { queryClient } from '@/services/query-client'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
|
|
||||||
export const useProfiles = () => {
|
export const useProfiles = () => {
|
||||||
const {
|
const {
|
||||||
data: profiles,
|
data: profiles,
|
||||||
mutate: mutateProfiles,
|
refetch,
|
||||||
error,
|
error,
|
||||||
isValidating,
|
isFetching: isValidating,
|
||||||
} = useSWR('getProfiles', getProfiles, {
|
} = useQuery({
|
||||||
revalidateOnFocus: false,
|
queryKey: ['getProfiles'],
|
||||||
revalidateOnReconnect: false,
|
queryFn: async () => {
|
||||||
dedupingInterval: 500, // 减少去重时间,提高响应性
|
const data = await getProfiles()
|
||||||
errorRetryCount: 3,
|
|
||||||
errorRetryInterval: 1000,
|
|
||||||
refreshInterval: 0, // 完全由手动控制
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('[useProfiles] SWR错误:', error)
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
debugLog(
|
debugLog(
|
||||||
'[useProfiles] 配置数据更新成功,配置数量:',
|
'[useProfiles] 配置数据更新成功,配置数量:',
|
||||||
data?.items?.length || 0,
|
data?.items?.length || 0,
|
||||||
)
|
)
|
||||||
|
return data
|
||||||
},
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
staleTime: 500,
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
refetchInterval: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mutateProfiles = async () => {
|
||||||
|
await refetch()
|
||||||
|
}
|
||||||
|
|
||||||
const patchProfiles = async (
|
const patchProfiles = async (
|
||||||
value: Partial<IProfilesConfig>,
|
value: Partial<IProfilesConfig>,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
@ -105,8 +110,14 @@ export const useProfiles = () => {
|
|||||||
`[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`,
|
`[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SelectedEntry = { name?: string; now?: string }
|
||||||
const selectedMap = Object.fromEntries(
|
const selectedMap = Object.fromEntries(
|
||||||
selected.map((each) => [each.name!, each.now!]),
|
(selected as SelectedEntry[])
|
||||||
|
.filter(
|
||||||
|
(each): each is SelectedEntry & { name: string; now: string } =>
|
||||||
|
each.name != null && each.now != null,
|
||||||
|
)
|
||||||
|
.map((each) => [each.name, each.now]),
|
||||||
)
|
)
|
||||||
|
|
||||||
let hasChange = false
|
let hasChange = false
|
||||||
@ -168,10 +179,10 @@ export const useProfiles = () => {
|
|||||||
hasChange = true
|
hasChange = true
|
||||||
try {
|
try {
|
||||||
await selectNodeForGroup(name, savedProxy)
|
await selectNodeForGroup(name, savedProxy)
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[ActivateSelected] 切换代理组 ${name} 失败:`,
|
`[ActivateSelected] 切换代理组 ${name} 失败:`,
|
||||||
error.message,
|
error instanceof Error ? error.message : String(error),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,15 +198,21 @@ export const useProfiles = () => {
|
|||||||
debugLog(`[ActivateSelected] 完成代理切换,保存新的选择配置`)
|
debugLog(`[ActivateSelected] 完成代理切换,保存新的选择配置`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchProfile(profileData.current!, { selected: newSelected })
|
await patchProfile(current.uid, { selected: newSelected })
|
||||||
debugLog('[ActivateSelected] 代理选择配置保存成功')
|
debugLog('[ActivateSelected] 代理选择配置保存成功')
|
||||||
|
|
||||||
await mutate('getProxies', calcuProxies())
|
queryClient.setQueryData(['getProxies'], await calcuProxies())
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('[ActivateSelected] 保存代理选择配置失败:', error.message)
|
console.error(
|
||||||
|
'[ActivateSelected] 保存代理选择配置失败:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('[ActivateSelected] 处理代理选择失败:', error.message)
|
console.error(
|
||||||
|
'[ActivateSelected] 处理代理选择失败:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import useSWR, { mutate } from 'swr'
|
|
||||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useAppData } from '@/providers/app-data-context'
|
import { useAppData } from '@/providers/app-data-context'
|
||||||
import { getAutotemProxy } from '@/services/cmds'
|
import { getAutotemProxy } from '@/services/cmds'
|
||||||
|
import { queryClient } from '@/services/query-client'
|
||||||
|
|
||||||
// 系统代理状态检测统一逻辑
|
// 系统代理状态检测统一逻辑
|
||||||
export const useSystemProxyState = () => {
|
export const useSystemProxyState = () => {
|
||||||
const { verge, mutateVerge, patchVerge } = useVerge()
|
const { verge, mutateVerge, patchVerge } = useVerge()
|
||||||
const { sysproxy, clashConfig } = useAppData()
|
const { sysproxy, clashConfig } = useAppData()
|
||||||
const { data: autoproxy } = useSWR('getAutotemProxy', getAutotemProxy, {
|
const { data: autoproxy } = useQuery({
|
||||||
revalidateOnFocus: true,
|
queryKey: ['getAutotemProxy'],
|
||||||
revalidateOnReconnect: true,
|
queryFn: getAutotemProxy,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnReconnect: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -41,7 +44,10 @@ export const useSystemProxyState = () => {
|
|||||||
const busyRef = useRef(false)
|
const busyRef = useRef(false)
|
||||||
|
|
||||||
const toggleSystemProxy = async (enabled: boolean) => {
|
const toggleSystemProxy = async (enabled: boolean) => {
|
||||||
mutateVerge({ ...verge, enable_system_proxy: enabled }, false)
|
mutateVerge(
|
||||||
|
(prev) => (prev ? { ...prev, enable_system_proxy: enabled } : prev),
|
||||||
|
false,
|
||||||
|
)
|
||||||
pendingRef.current = enabled
|
pendingRef.current = enabled
|
||||||
|
|
||||||
if (busyRef.current) return
|
if (busyRef.current) return
|
||||||
@ -58,13 +64,23 @@ export const useSystemProxyState = () => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
busyRef.current = false
|
busyRef.current = false
|
||||||
await Promise.all([mutate('getSystemProxy'), mutate('getAutotemProxy')])
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getSystemProxy'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getAutotemProxy'] }),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const invalidateProxyState = () =>
|
||||||
|
Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getSystemProxy'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getAutotemProxy'] }),
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
indicator,
|
indicator,
|
||||||
configState: enable_system_proxy ?? false,
|
configState: enable_system_proxy ?? false,
|
||||||
toggleSystemProxy,
|
toggleSystemProxy,
|
||||||
|
invalidateProxyState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import useSWR from 'swr'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds'
|
import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
@ -18,6 +18,9 @@ const defaultSystemState = {
|
|||||||
isServiceOk: false,
|
isServiceOk: false,
|
||||||
} as SystemState
|
} as SystemState
|
||||||
|
|
||||||
|
// Grace period for service initialization during startup
|
||||||
|
const STARTUP_GRACE_MS = 10_000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义 hook 用于获取系统运行状态
|
* 自定义 hook 用于获取系统运行状态
|
||||||
* 包括运行模式、管理员状态、系统服务是否可用
|
* 包括运行模式、管理员状态、系统服务是否可用
|
||||||
@ -25,14 +28,20 @@ const defaultSystemState = {
|
|||||||
export function useSystemState() {
|
export function useSystemState() {
|
||||||
const { verge, patchVerge } = useVerge()
|
const { verge, patchVerge } = useVerge()
|
||||||
const disablingTunRef = useRef(false)
|
const disablingTunRef = useRef(false)
|
||||||
|
const [isStartingUp, setIsStartingUp] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setIsStartingUp(false), STARTUP_GRACE_MS)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: systemState,
|
data: systemState = defaultSystemState,
|
||||||
mutate: mutateSystemState,
|
refetch: mutateSystemState,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSWR(
|
} = useQuery({
|
||||||
'getSystemState',
|
queryKey: ['getSystemState'],
|
||||||
async () => {
|
queryFn: async () => {
|
||||||
const [runningMode, isAdminMode, isServiceOk] = await Promise.all([
|
const [runningMode, isAdminMode, isServiceOk] = await Promise.all([
|
||||||
getRunningMode(),
|
getRunningMode(),
|
||||||
isAdmin(),
|
isAdmin(),
|
||||||
@ -40,12 +49,8 @@ export function useSystemState() {
|
|||||||
])
|
])
|
||||||
return { runningMode, isAdminMode, isServiceOk } as SystemState
|
return { runningMode, isAdminMode, isServiceOk } as SystemState
|
||||||
},
|
},
|
||||||
{
|
refetchInterval: isStartingUp ? 2000 : 30000,
|
||||||
suspense: true,
|
})
|
||||||
refreshInterval: 30000,
|
|
||||||
fallback: defaultSystemState,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const isSidecarMode = systemState.runningMode === 'Sidecar'
|
const isSidecarMode = systemState.runningMode === 'Sidecar'
|
||||||
const isServiceMode = systemState.runningMode === 'Service'
|
const isServiceMode = systemState.runningMode === 'Service'
|
||||||
@ -61,7 +66,8 @@ export function useSystemState() {
|
|||||||
!disablingTunRef.current &&
|
!disablingTunRef.current &&
|
||||||
enable_tun_mode &&
|
enable_tun_mode &&
|
||||||
!isTunModeAvailable &&
|
!isTunModeAvailable &&
|
||||||
!isLoading
|
!isLoading &&
|
||||||
|
!isStartingUp
|
||||||
) {
|
) {
|
||||||
disablingTunRef.current = true
|
disablingTunRef.current = true
|
||||||
patchVerge({ enable_tun_mode: false })
|
patchVerge({ enable_tun_mode: false })
|
||||||
@ -92,7 +98,7 @@ export function useSystemState() {
|
|||||||
disablingTunRef.current = false
|
disablingTunRef.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading])
|
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading, isStartingUp])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
runningMode: systemState.runningMode,
|
runningMode: systemState.runningMode,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import useSWR, { mutate as globalMutate, SWRConfiguration } from 'swr'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import { queryClient } from '@/services/query-client'
|
||||||
import { checkUpdateSafe } from '@/services/update'
|
import { checkUpdateSafe } from '@/services/update'
|
||||||
|
|
||||||
import { useVerge } from './use-verge'
|
import { useVerge } from './use-verge'
|
||||||
@ -12,8 +13,6 @@ export interface UpdateInfo {
|
|||||||
downloadAndInstall: (onEvent?: any) => Promise<void>
|
downloadAndInstall: (onEvent?: any) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Last check timestamp (shared via SWR + localStorage) ---
|
|
||||||
|
|
||||||
const LAST_CHECK_KEY = 'last_check_update'
|
const LAST_CHECK_KEY = 'last_check_update'
|
||||||
|
|
||||||
export const readLastCheckTime = (): number | null => {
|
export const readLastCheckTime = (): number | null => {
|
||||||
@ -26,16 +25,13 @@ export const readLastCheckTime = (): number | null => {
|
|||||||
export const updateLastCheckTime = (timestamp?: number): number => {
|
export const updateLastCheckTime = (timestamp?: number): number => {
|
||||||
const now = timestamp ?? Date.now()
|
const now = timestamp ?? Date.now()
|
||||||
localStorage.setItem(LAST_CHECK_KEY, now.toString())
|
localStorage.setItem(LAST_CHECK_KEY, now.toString())
|
||||||
globalMutate(LAST_CHECK_KEY, now, false)
|
queryClient.setQueryData([LAST_CHECK_KEY], now)
|
||||||
return now
|
return now
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- useUpdate hook ---
|
// --- useUpdate hook ---
|
||||||
|
|
||||||
export const useUpdate = (
|
export const useUpdate = (enabled: boolean = true) => {
|
||||||
enabled: boolean = true,
|
|
||||||
options?: SWRConfiguration,
|
|
||||||
) => {
|
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { auto_check_update } = verge || {}
|
const { auto_check_update } = verge || {}
|
||||||
|
|
||||||
@ -46,26 +42,28 @@ export const useUpdate = (
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data: updateInfo,
|
data: updateInfo,
|
||||||
mutate: checkUpdate,
|
refetch: checkUpdate,
|
||||||
isValidating,
|
isFetching: isValidating,
|
||||||
} = useSWR(shouldCheck ? 'checkUpdate' : null, checkUpdateSafe, {
|
} = useQuery({
|
||||||
errorRetryCount: 2,
|
queryKey: ['checkUpdate'],
|
||||||
revalidateIfStale: false,
|
queryFn: async () => {
|
||||||
revalidateOnFocus: false,
|
const result = await checkUpdateSafe()
|
||||||
focusThrottleInterval: 36e5, // 1 hour
|
|
||||||
refreshInterval: 24 * 60 * 60 * 1000, // 24 hours
|
|
||||||
dedupingInterval: 60 * 60 * 1000, // 1 hour
|
|
||||||
...options,
|
|
||||||
onSuccess: (...args) => {
|
|
||||||
updateLastCheckTime()
|
updateLastCheckTime()
|
||||||
options?.onSuccess?.(...args)
|
return result
|
||||||
},
|
},
|
||||||
|
enabled: shouldCheck,
|
||||||
|
retry: 2,
|
||||||
|
staleTime: 60 * 60 * 1000,
|
||||||
|
refetchInterval: 24 * 60 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Shared last check timestamp
|
// Shared last check timestamp
|
||||||
const { data: lastCheckUpdate } = useSWR(LAST_CHECK_KEY, readLastCheckTime, {
|
const { data: lastCheckUpdate } = useQuery({
|
||||||
revalidateOnFocus: false,
|
queryKey: [LAST_CHECK_KEY],
|
||||||
revalidateOnReconnect: false,
|
queryFn: readLastCheckTime,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,28 +1,52 @@
|
|||||||
import useSWR from 'swr'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
import { getVergeConfig, patchVergeConfig } from '@/services/cmds'
|
import { getVergeConfig, patchVergeConfig } from '@/services/cmds'
|
||||||
import { getPreloadConfig, setPreloadConfig } from '@/services/preload'
|
import { getPreloadConfig, setPreloadConfig } from '@/services/preload'
|
||||||
|
|
||||||
export const useVerge = () => {
|
export const useVerge = () => {
|
||||||
|
const qc = useQueryClient()
|
||||||
const initialVergeConfig = getPreloadConfig()
|
const initialVergeConfig = getPreloadConfig()
|
||||||
const { data: verge, mutate: mutateVerge } = useSWR(
|
|
||||||
'getVergeConfig',
|
const { data: verge, refetch } = useQuery({
|
||||||
async () => {
|
queryKey: ['getVergeConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
const config = await getVergeConfig()
|
const config = await getVergeConfig()
|
||||||
setPreloadConfig(config)
|
setPreloadConfig(config)
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
{
|
initialData: initialVergeConfig ?? undefined,
|
||||||
fallbackData: initialVergeConfig ?? undefined,
|
staleTime: 5000,
|
||||||
revalidateOnMount: !initialVergeConfig,
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const patchVerge = async (value: Partial<IVergeConfig>) => {
|
const mutateVerge = (
|
||||||
await patchVergeConfig(value)
|
updaterOrData?:
|
||||||
mutateVerge()
|
| IVergeConfig
|
||||||
|
| ((prev: IVergeConfig | undefined) => IVergeConfig | undefined)
|
||||||
|
| undefined,
|
||||||
|
_revalidate?: boolean,
|
||||||
|
) => {
|
||||||
|
if (updaterOrData === undefined) {
|
||||||
|
void refetch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof updaterOrData === 'function') {
|
||||||
|
const prev = qc.getQueryData<IVergeConfig>(['getVergeConfig'])
|
||||||
|
const next = updaterOrData(prev)
|
||||||
|
qc.setQueryData(['getVergeConfig'], next)
|
||||||
|
} else {
|
||||||
|
qc.setQueryData(['getVergeConfig'], updaterOrData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const patchVerge = useCallback(
|
||||||
|
async (value: Partial<IVergeConfig>) => {
|
||||||
|
await patchVergeConfig(value)
|
||||||
|
await refetch()
|
||||||
|
},
|
||||||
|
[refetch],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
verge,
|
verge,
|
||||||
mutateVerge,
|
mutateVerge,
|
||||||
|
|||||||
14
src/main.tsx
14
src/main.tsx
@ -2,6 +2,7 @@ import './assets/styles/index.scss'
|
|||||||
import './services/monaco'
|
import './services/monaco'
|
||||||
|
|
||||||
import { ResizeObserver } from '@juggle/resize-observer'
|
import { ResizeObserver } from '@juggle/resize-observer'
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { ComposeContextProvider } from 'foxact/compose-context-provider'
|
import { ComposeContextProvider } from 'foxact/compose-context-provider'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
resolveThemeMode,
|
resolveThemeMode,
|
||||||
getPreloadConfig,
|
getPreloadConfig,
|
||||||
} from './services/preload'
|
} from './services/preload'
|
||||||
|
import { queryClient } from './services/query-client'
|
||||||
import {
|
import {
|
||||||
LoadingCacheProvider,
|
LoadingCacheProvider,
|
||||||
ThemeModeProvider,
|
ThemeModeProvider,
|
||||||
@ -50,11 +52,13 @@ const initializeApp = (initialThemeMode: 'light' | 'dark') => {
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ComposeContextProvider contexts={contexts}>
|
<ComposeContextProvider contexts={contexts}>
|
||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
<WindowProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppDataProvider>
|
<WindowProvider>
|
||||||
<RouterProvider router={router} />
|
<AppDataProvider>
|
||||||
</AppDataProvider>
|
<RouterProvider router={router} />
|
||||||
</WindowProvider>
|
</AppDataProvider>
|
||||||
|
</WindowProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</BaseErrorBoundary>
|
</BaseErrorBoundary>
|
||||||
</ComposeContextProvider>
|
</ComposeContextProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import type { CSSProperties } from 'react'
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||||
import { SWRConfig } from 'swr'
|
|
||||||
|
|
||||||
import iconDark from '@/assets/image/icon_dark.svg?react'
|
import iconDark from '@/assets/image/icon_dark.svg?react'
|
||||||
import iconLight from '@/assets/image/icon_light.svg?react'
|
import iconLight from '@/assets/image/icon_light.svg?react'
|
||||||
@ -259,249 +258,220 @@ const Layout = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRConfig
|
<ThemeProvider theme={theme}>
|
||||||
value={{
|
{/* 左侧底部窗口控制按钮 */}
|
||||||
errorRetryCount: 3,
|
<NoticeManager position={verge?.notice_position} />
|
||||||
// TODO remove the 5000ms
|
<div
|
||||||
errorRetryInterval: 5000,
|
style={{
|
||||||
onError: (error, key) => {
|
animation: 'fadeIn 0.5s',
|
||||||
// FIXME the condition should not be handle gllobally
|
WebkitAnimation: 'fadeIn 0.5s',
|
||||||
if (key !== 'getAutotemProxy') {
|
}}
|
||||||
console.error(`SWR Error for ${key}:`, error)
|
/>
|
||||||
return
|
<style>
|
||||||
}
|
{`
|
||||||
|
|
||||||
// FIXME we need a better way to handle the retry when first booting app
|
|
||||||
const silentKeys = ['getVersion', 'getClashConfig', 'getAutotemProxy']
|
|
||||||
if (silentKeys.includes(key)) return
|
|
||||||
|
|
||||||
console.error(`[SWR Error] Key: ${key}, Error:`, error)
|
|
||||||
},
|
|
||||||
dedupingInterval: 2000,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
{/* 左侧底部窗口控制按钮 */}
|
|
||||||
<NoticeManager position={verge?.notice_position} />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
animation: 'fadeIn 0.5s',
|
|
||||||
WebkitAnimation: 'fadeIn 0.5s',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
{`
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
<Paper
|
<Paper
|
||||||
square
|
square
|
||||||
elevation={0}
|
elevation={0}
|
||||||
className={`${OS} layout${navCollapsed ? ' layout--nav-collapsed' : ''}`}
|
className={`${OS} layout${navCollapsed ? ' layout--nav-collapsed' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
borderTopLeftRadius: '0px',
|
borderTopLeftRadius: '0px',
|
||||||
borderTopRightRadius: '0px',
|
borderTopRightRadius: '0px',
|
||||||
}}
|
}}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
if (
|
if (
|
||||||
OS === 'windows' &&
|
OS === 'windows' &&
|
||||||
!['input', 'textarea'].includes(
|
!['input', 'textarea'].includes(
|
||||||
e.currentTarget.tagName.toLowerCase(),
|
e.currentTarget.tagName.toLowerCase(),
|
||||||
) &&
|
) &&
|
||||||
!e.currentTarget.isContentEditable
|
!e.currentTarget.isContentEditable
|
||||||
) {
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={[
|
sx={[
|
||||||
({ palette }) => ({ bgcolor: palette.background.paper }),
|
({ palette }) => ({ bgcolor: palette.background.paper }),
|
||||||
OS === 'linux'
|
OS === 'linux'
|
||||||
? {
|
? {
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{/* Custom titlebar - rendered only when decorated is false, memoized for performance */}
|
{/* Custom titlebar - rendered only when decorated is false, memoized for performance */}
|
||||||
{customTitlebar}
|
{customTitlebar}
|
||||||
|
|
||||||
<div className="layout-content">
|
<div className="layout-content">
|
||||||
<div className="layout-content__left">
|
<div className="layout-content__left">
|
||||||
<div className="the-logo" data-tauri-drag-region="false">
|
<div className="the-logo" data-tauri-drag-region="false">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region="true"
|
data-tauri-drag-region="true"
|
||||||
style={{
|
style={{
|
||||||
height: '27px',
|
height: '27px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SvgIcon
|
|
||||||
component={isDark ? iconDark : iconLight}
|
|
||||||
style={{
|
|
||||||
height: '36px',
|
|
||||||
width: '36px',
|
|
||||||
marginTop: '-3px',
|
|
||||||
marginRight: '5px',
|
|
||||||
marginLeft: '-3px',
|
|
||||||
}}
|
|
||||||
inheritViewBox
|
|
||||||
/>
|
|
||||||
<LogoSvg fill={isDark ? 'white' : 'black'} />
|
|
||||||
</div>
|
|
||||||
<UpdateButton className="the-newbtn" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{menuUnlocked && (
|
|
||||||
<Box
|
|
||||||
sx={(theme) => ({
|
|
||||||
px: 1.5,
|
|
||||||
py: 0.75,
|
|
||||||
mx: 'auto',
|
|
||||||
mb: 1,
|
|
||||||
maxWidth: 250,
|
|
||||||
borderRadius: 1.5,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
textAlign: 'center',
|
|
||||||
color: theme.palette.warning.contrastText,
|
|
||||||
bgcolor:
|
|
||||||
theme.palette.mode === 'light'
|
|
||||||
? theme.palette.warning.main
|
|
||||||
: theme.palette.warning.dark,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('layout.components.navigation.menu.reorderMode')}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{menuUnlocked ? (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={handleMenuDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext items={menuOrder}>
|
|
||||||
<List
|
|
||||||
className="the-menu"
|
|
||||||
onContextMenu={handleMenuContextMenu}
|
|
||||||
>
|
|
||||||
{menuOrder.map((path) => {
|
|
||||||
const item = navItemMap.get(path)
|
|
||||||
if (!item) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<SortableNavMenuItem
|
|
||||||
key={item.path}
|
|
||||||
item={item}
|
|
||||||
label={t(item.label)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
) : (
|
|
||||||
<List
|
|
||||||
className="the-menu"
|
|
||||||
onContextMenu={handleMenuContextMenu}
|
|
||||||
>
|
|
||||||
{menuOrder.map((path) => {
|
|
||||||
const item = navItemMap.get(path)
|
|
||||||
if (!item) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<LayoutItem
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
icon={item.icon}
|
|
||||||
>
|
|
||||||
{t(item.label)}
|
|
||||||
</LayoutItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Menu
|
|
||||||
open={Boolean(menuContextPosition)}
|
|
||||||
onClose={handleMenuContextClose}
|
|
||||||
anchorReference="anchorPosition"
|
|
||||||
anchorPosition={
|
|
||||||
menuContextPosition
|
|
||||||
? {
|
|
||||||
top: menuContextPosition.top,
|
|
||||||
left: menuContextPosition.left,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
transitionDuration={200}
|
|
||||||
slotProps={{
|
|
||||||
list: {
|
|
||||||
sx: { py: 0.5 },
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleToggleNavCollapsed} dense>
|
<SvgIcon
|
||||||
{navCollapsed
|
component={isDark ? iconDark : iconLight}
|
||||||
? t('layout.components.navigation.menu.expandNavBar')
|
style={{
|
||||||
: t('layout.components.navigation.menu.collapseNavBar')}
|
height: '36px',
|
||||||
</MenuItem>
|
width: '36px',
|
||||||
<MenuItem
|
marginTop: '-3px',
|
||||||
onClick={menuUnlocked ? handleLockMenu : handleUnlockMenu}
|
marginRight: '5px',
|
||||||
dense
|
marginLeft: '-3px',
|
||||||
>
|
}}
|
||||||
{menuUnlocked
|
inheritViewBox
|
||||||
? t('layout.components.navigation.menu.lock')
|
/>
|
||||||
: t('layout.components.navigation.menu.unlock')}
|
<LogoSvg fill={isDark ? 'white' : 'black'} />
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={handleResetMenuOrder}
|
|
||||||
dense
|
|
||||||
disabled={isDefaultOrder}
|
|
||||||
>
|
|
||||||
{t('layout.components.navigation.menu.restoreDefaultOrder')}
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<div className="the-traffic">
|
|
||||||
<LayoutTraffic />
|
|
||||||
</div>
|
</div>
|
||||||
|
<UpdateButton className="the-newbtn" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="layout-content__right">
|
{menuUnlocked && (
|
||||||
<div className="the-bar"></div>
|
<Box
|
||||||
<div className="the-content">
|
sx={(theme) => ({
|
||||||
<BaseErrorBoundary>
|
px: 1.5,
|
||||||
<Outlet />
|
py: 0.75,
|
||||||
</BaseErrorBoundary>
|
mx: 'auto',
|
||||||
{logsPageMountedRef.current && (
|
mb: 1,
|
||||||
<div
|
maxWidth: 250,
|
||||||
style={{
|
borderRadius: 1.5,
|
||||||
position: 'absolute',
|
fontSize: 12,
|
||||||
top: 0,
|
fontWeight: 600,
|
||||||
left: 0,
|
textAlign: 'center',
|
||||||
right: 0,
|
color: theme.palette.warning.contrastText,
|
||||||
bottom: 0,
|
bgcolor:
|
||||||
display: isLogsPage ? undefined : 'none',
|
theme.palette.mode === 'light'
|
||||||
}}
|
? theme.palette.warning.main
|
||||||
|
: theme.palette.warning.dark,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('layout.components.navigation.menu.reorderMode')}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{menuUnlocked ? (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleMenuDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext items={menuOrder}>
|
||||||
|
<List
|
||||||
|
className="the-menu"
|
||||||
|
onContextMenu={handleMenuContextMenu}
|
||||||
>
|
>
|
||||||
<LogsPage />
|
{menuOrder.map((path) => {
|
||||||
</div>
|
const item = navItemMap.get(path)
|
||||||
)}
|
if (!item) {
|
||||||
</div>
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SortableNavMenuItem
|
||||||
|
key={item.path}
|
||||||
|
item={item}
|
||||||
|
label={t(item.label)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
) : (
|
||||||
|
<List className="the-menu" onContextMenu={handleMenuContextMenu}>
|
||||||
|
{menuOrder.map((path) => {
|
||||||
|
const item = navItemMap.get(path)
|
||||||
|
if (!item) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<LayoutItem key={item.path} to={item.path} icon={item.icon}>
|
||||||
|
{t(item.label)}
|
||||||
|
</LayoutItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
open={Boolean(menuContextPosition)}
|
||||||
|
onClose={handleMenuContextClose}
|
||||||
|
anchorReference="anchorPosition"
|
||||||
|
anchorPosition={
|
||||||
|
menuContextPosition
|
||||||
|
? {
|
||||||
|
top: menuContextPosition.top,
|
||||||
|
left: menuContextPosition.left,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
transitionDuration={200}
|
||||||
|
slotProps={{
|
||||||
|
list: {
|
||||||
|
sx: { py: 0.5 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleToggleNavCollapsed} dense>
|
||||||
|
{navCollapsed
|
||||||
|
? t('layout.components.navigation.menu.expandNavBar')
|
||||||
|
: t('layout.components.navigation.menu.collapseNavBar')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={menuUnlocked ? handleLockMenu : handleUnlockMenu}
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
{menuUnlocked
|
||||||
|
? t('layout.components.navigation.menu.lock')
|
||||||
|
: t('layout.components.navigation.menu.unlock')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleResetMenuOrder}
|
||||||
|
dense
|
||||||
|
disabled={isDefaultOrder}
|
||||||
|
>
|
||||||
|
{t('layout.components.navigation.menu.restoreDefaultOrder')}
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<div className="the-traffic">
|
||||||
|
<LayoutTraffic />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
|
||||||
</ThemeProvider>
|
<div className="layout-content__right">
|
||||||
</SWRConfig>
|
<div className="the-bar"></div>
|
||||||
|
<div className="the-content">
|
||||||
|
<BaseErrorBoundary>
|
||||||
|
<Outlet />
|
||||||
|
</BaseErrorBoundary>
|
||||||
|
{logsPageMountedRef.current && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: isLogsPage ? undefined : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogsPage />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { mutate } from 'swr'
|
|
||||||
|
|
||||||
import { useListen } from '@/hooks/use-listen'
|
import { useListen } from '@/hooks/use-listen'
|
||||||
|
import { queryClient } from '@/services/query-client'
|
||||||
|
|
||||||
export const useLayoutEvents = (
|
export const useLayoutEvents = (
|
||||||
handleNotice: (payload: [string, string]) => void,
|
handleNotice: (payload: [string, string]) => void,
|
||||||
@ -14,8 +14,9 @@ export const useLayoutEvents = (
|
|||||||
const unlisteners: Array<() => void> = []
|
const unlisteners: Array<() => void> = []
|
||||||
let disposed = false
|
let disposed = false
|
||||||
const revalidateKeys = (keys: readonly string[]) => {
|
const revalidateKeys = (keys: readonly string[]) => {
|
||||||
const keySet = new Set(keys)
|
keys.forEach((key) => {
|
||||||
mutate((key) => typeof key === 'string' && keySet.has(key))
|
queryClient.invalidateQueries({ queryKey: [key] })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const register = (
|
const register = (
|
||||||
@ -61,6 +62,7 @@ export const useLayoutEvents = (
|
|||||||
'getAutotemProxy',
|
'getAutotemProxy',
|
||||||
'getRunningMode',
|
'getRunningMode',
|
||||||
'isServiceAvailable',
|
'isServiceAvailable',
|
||||||
|
'getSystemState',
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import {
|
|||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
|
||||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -23,6 +22,7 @@ import {
|
|||||||
BasePage,
|
BasePage,
|
||||||
BaseSearchBox,
|
BaseSearchBox,
|
||||||
BaseStyledSelect,
|
BaseStyledSelect,
|
||||||
|
VirtualList,
|
||||||
} from '@/components/base'
|
} from '@/components/base'
|
||||||
import {
|
import {
|
||||||
ConnectionDetail,
|
ConnectionDetail,
|
||||||
@ -243,23 +243,27 @@ const ConnectionsPage = () => {
|
|||||||
onCloseColumnManager={() => setIsColumnManagerOpen(false)}
|
onCloseColumnManager={() => setIsColumnManagerOpen(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Virtuoso
|
<VirtualList
|
||||||
|
count={filterConn.length}
|
||||||
|
estimateSize={56}
|
||||||
|
renderItem={(i) => (
|
||||||
|
<ConnectionItem
|
||||||
|
value={filterConn[i]}
|
||||||
|
closed={connectionsType === 'closed'}
|
||||||
|
onShowDetail={() =>
|
||||||
|
detailRef.current?.open(
|
||||||
|
filterConn[i],
|
||||||
|
connectionsType === 'closed',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
overscrollBehavior: 'contain',
|
overscrollBehavior: 'contain',
|
||||||
}}
|
}}
|
||||||
data={filterConn}
|
|
||||||
itemContent={(_, item) => (
|
|
||||||
<ConnectionItem
|
|
||||||
value={item}
|
|
||||||
closed={connectionsType === 'closed'}
|
|
||||||
onShowDetail={() =>
|
|
||||||
detailRef.current?.open(item, connectionsType === 'closed')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ConnectionDetail ref={detailRef} />
|
<ConnectionDetail ref={detailRef} />
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import {
|
|||||||
SwapVertRounded,
|
SwapVertRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, Button, IconButton, MenuItem } from '@mui/material'
|
import { Box, Button, IconButton, MenuItem } from '@mui/material'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BaseEmpty,
|
BaseEmpty,
|
||||||
@ -14,6 +13,8 @@ import {
|
|||||||
BaseSearchBox,
|
BaseSearchBox,
|
||||||
BaseStyledSelect,
|
BaseStyledSelect,
|
||||||
type SearchState,
|
type SearchState,
|
||||||
|
VirtualList,
|
||||||
|
type VirtualListHandle,
|
||||||
} from '@/components/base'
|
} from '@/components/base'
|
||||||
import LogItem from '@/components/log/log-item'
|
import LogItem from '@/components/log/log-item'
|
||||||
import { useClashLog } from '@/hooks/use-clash-log'
|
import { useClashLog } from '@/hooks/use-clash-log'
|
||||||
@ -60,6 +61,16 @@ const LogPage = () => {
|
|||||||
[filterLogs, isDescending],
|
[filterLogs, isDescending],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const virtuosoRef = useRef<VirtualListHandle>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDescending && filteredLogs.length > 0) {
|
||||||
|
virtuosoRef.current?.scrollToIndex(filteredLogs.length - 1, {
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [filteredLogs.length, isDescending])
|
||||||
|
|
||||||
const handleLogLevelChange = (newLevel: string) => {
|
const handleLogLevelChange = (newLevel: string) => {
|
||||||
setClashLog((pre: any) => ({ ...pre, logFilter: newLevel }))
|
setClashLog((pre: any) => ({ ...pre, logFilter: newLevel }))
|
||||||
}
|
}
|
||||||
@ -170,16 +181,14 @@ const LogPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{filteredLogs.length > 0 ? (
|
{filteredLogs.length > 0 ? (
|
||||||
<Virtuoso
|
<VirtualList
|
||||||
initialTopMostItemIndex={isDescending ? 0 : 999}
|
ref={virtuosoRef}
|
||||||
data={filteredLogs}
|
count={filteredLogs.length}
|
||||||
style={{
|
estimateSize={50}
|
||||||
flex: 1,
|
renderItem={(i) => (
|
||||||
}}
|
<LogItem value={filteredLogs[i]} searchState={searchState} />
|
||||||
itemContent={(index, item) => (
|
|
||||||
<LogItem value={item} searchState={searchState} />
|
|
||||||
)}
|
)}
|
||||||
followOutput={isDescending ? false : 'smooth'}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BaseEmpty />
|
<BaseEmpty />
|
||||||
|
|||||||
@ -22,15 +22,22 @@ import {
|
|||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { LoadingButton } from '@mui/lab'
|
import { LoadingButton } from '@mui/lab'
|
||||||
import { Box, Button, Divider, Grid, IconButton, Stack } from '@mui/material'
|
import { Box, Button, Divider, Grid, IconButton, Stack } from '@mui/material'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { listen, TauriEvent } from '@tauri-apps/api/event'
|
import { listen, TauriEvent } from '@tauri-apps/api/event'
|
||||||
import { readText } from '@tauri-apps/plugin-clipboard-manager'
|
import { readText } from '@tauri-apps/plugin-clipboard-manager'
|
||||||
import { readTextFile } from '@tauri-apps/plugin-fs'
|
import { readTextFile } from '@tauri-apps/plugin-fs'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import { throttle } from 'lodash-es'
|
import { throttle } from 'lodash-es'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type RefObject,
|
||||||
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLocation } from 'react-router'
|
import { useLocation } from 'react-router'
|
||||||
import useSWR, { mutate } from 'swr'
|
|
||||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { BasePage, BaseStyledTextField, DialogRef } from '@/components/base'
|
import { BasePage, BaseStyledTextField, DialogRef } from '@/components/base'
|
||||||
@ -55,6 +62,7 @@ import {
|
|||||||
updateProfile,
|
updateProfile,
|
||||||
} from '@/services/cmds'
|
} from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
import { queryClient } from '@/services/query-client'
|
||||||
import { useSetLoadingCache, useThemeMode } from '@/services/states'
|
import { useSetLoadingCache, useThemeMode } from '@/services/states'
|
||||||
import { debugLog } from '@/utils/debug'
|
import { debugLog } from '@/utils/debug'
|
||||||
|
|
||||||
@ -67,7 +75,7 @@ const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
|
|||||||
// 检查请求是否已过期
|
// 检查请求是否已过期
|
||||||
const isRequestOutdated = (
|
const isRequestOutdated = (
|
||||||
currentSequence: number,
|
currentSequence: number,
|
||||||
requestSequenceRef: any,
|
requestSequenceRef: RefObject<number>,
|
||||||
profile: string,
|
profile: string,
|
||||||
) => {
|
) => {
|
||||||
if (currentSequence !== requestSequenceRef.current) {
|
if (currentSequence !== requestSequenceRef.current) {
|
||||||
@ -222,14 +230,14 @@ const ProfilePage = () => {
|
|||||||
debugLog('[紧急刷新] 开始强制刷新所有数据')
|
debugLog('[紧急刷新] 开始强制刷新所有数据')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 清除所有SWR缓存
|
// 只失效 profiles 相关 query,不影响 WS 订阅、IP 缓存等其他 query
|
||||||
await mutate(() => true, undefined, { revalidate: false })
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getProfiles'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['getRuntimeLogs'] }),
|
||||||
|
])
|
||||||
|
|
||||||
// 强制重新获取配置数据
|
// 强制重新获取配置数据
|
||||||
await mutateProfiles(undefined, {
|
await mutateProfiles()
|
||||||
revalidate: true,
|
|
||||||
rollbackOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 等待状态稳定后增强配置
|
// 等待状态稳定后增强配置
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
@ -249,10 +257,10 @@ const ProfilePage = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
|
const { data: chainLogs = {}, refetch: mutateLogs } = useQuery({
|
||||||
'getRuntimeLogs',
|
queryKey: ['getRuntimeLogs'],
|
||||||
getRuntimeLogs,
|
queryFn: getRuntimeLogs,
|
||||||
)
|
})
|
||||||
|
|
||||||
const viewerRef = useRef<ProfileViewerRef>(null)
|
const viewerRef = useRef<ProfileViewerRef>(null)
|
||||||
const configRef = useRef<DialogRef>(null)
|
const configRef = useRef<DialogRef>(null)
|
||||||
@ -316,9 +324,10 @@ const ProfilePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强化的刷新策略
|
// 强化的刷新策略
|
||||||
|
// maxRetries 设为 1:useProfiles 内部 useQuery 已配置 retry:3,业务层只需 1 次额外重试
|
||||||
const performRobustRefresh = async () => {
|
const performRobustRefresh = async () => {
|
||||||
let retryCount = 0
|
let retryCount = 0
|
||||||
const maxRetries = 5
|
const maxRetries = 1
|
||||||
const baseDelay = 200
|
const baseDelay = 200
|
||||||
|
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
@ -326,10 +335,7 @@ const ProfilePage = () => {
|
|||||||
debugLog(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`)
|
debugLog(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`)
|
||||||
|
|
||||||
// 强制刷新,绕过所有缓存
|
// 强制刷新,绕过所有缓存
|
||||||
await mutateProfiles(undefined, {
|
await mutateProfiles()
|
||||||
revalidate: true,
|
|
||||||
rollbackOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 等待状态稳定
|
// 等待状态稳定
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
@ -350,8 +356,11 @@ const ProfilePage = () => {
|
|||||||
// 所有重试失败后的最后尝试
|
// 所有重试失败后的最后尝试
|
||||||
console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`)
|
console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`)
|
||||||
try {
|
try {
|
||||||
// 清除SWR缓存并重新获取
|
// 清除缓存并重新获取
|
||||||
await mutate('getProfiles', getProfiles(), { revalidate: true })
|
await queryClient.fetchQuery({
|
||||||
|
queryKey: ['getProfiles'],
|
||||||
|
queryFn: getProfiles,
|
||||||
|
})
|
||||||
await onEnhance(false)
|
await onEnhance(false)
|
||||||
showNotice.error(
|
showNotice.error(
|
||||||
'profiles.page.feedback.notifications.importNeedsRefresh',
|
'profiles.page.feedback.notifications.importNeedsRefresh',
|
||||||
@ -1007,6 +1016,7 @@ const ProfilePage = () => {
|
|||||||
selected={profiles.current === item.uid}
|
selected={profiles.current === item.uid}
|
||||||
activating={activatings.includes(item.uid)}
|
activating={activatings.includes(item.uid)}
|
||||||
itemData={item}
|
itemData={item}
|
||||||
|
mutateProfiles={mutateProfiles}
|
||||||
onSelect={(f) => onSelect(item.uid, f)}
|
onSelect={(f) => onSelect(item.uid, f)}
|
||||||
onEdit={() => viewerRef.current?.edit(item)}
|
onEdit={() => viewerRef.current?.edit(item)}
|
||||||
onSave={async (prev, curr) => {
|
onSave={async (prev, curr) => {
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
|
|
||||||
|
|
||||||
import { BaseEmpty, BasePage, BaseSearchBox } from '@/components/base'
|
import {
|
||||||
|
BaseEmpty,
|
||||||
|
BasePage,
|
||||||
|
BaseSearchBox,
|
||||||
|
VirtualList,
|
||||||
|
type VirtualListHandle,
|
||||||
|
} from '@/components/base'
|
||||||
import { ScrollTopButton } from '@/components/layout/scroll-top-button'
|
import { ScrollTopButton } from '@/components/layout/scroll-top-button'
|
||||||
import { ProviderButton } from '@/components/rule/provider-button'
|
import { ProviderButton } from '@/components/rule/provider-button'
|
||||||
import RuleItem from '@/components/rule/rule-item'
|
import RuleItem from '@/components/rule/rule-item'
|
||||||
@ -14,7 +19,7 @@ const RulesPage = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { rules = [], refreshRules, refreshRuleProviders } = useAppData()
|
const { rules = [], refreshRules, refreshRuleProviders } = useAppData()
|
||||||
const [match, setMatch] = useState(() => (_: string) => true)
|
const [match, setMatch] = useState(() => (_: string) => true)
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
const virtuosoRef = useRef<VirtualListHandle>(null)
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
const pageVisible = useVisibility()
|
const pageVisible = useVisibility()
|
||||||
|
|
||||||
@ -39,15 +44,12 @@ const RulesPage = () => {
|
|||||||
return rulesWithLineNo.filter((item) => match(item.payload ?? ''))
|
return rulesWithLineNo.filter((item) => match(item.payload ?? ''))
|
||||||
}, [rules, match])
|
}, [rules, match])
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const handleScroll = useCallback((e: Event) => {
|
||||||
virtuosoRef.current?.scrollTo({
|
setShowScrollTop((e.target as HTMLElement).scrollTop > 100)
|
||||||
top: 0,
|
}, [])
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = (e: any) => {
|
const scrollToTop = () => {
|
||||||
setShowScrollTop(e.target.scrollTop > 100)
|
virtuosoRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -81,17 +83,13 @@ const RulesPage = () => {
|
|||||||
|
|
||||||
{filteredRules && filteredRules.length > 0 ? (
|
{filteredRules && filteredRules.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Virtuoso
|
<VirtualList
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={filteredRules}
|
count={filteredRules.length}
|
||||||
style={{
|
estimateSize={40}
|
||||||
flex: 1,
|
renderItem={(i) => <RuleItem value={filteredRules[i]} />}
|
||||||
}}
|
style={{ flex: 1 }}
|
||||||
itemContent={(_index, item) => <RuleItem value={item} />}
|
onScroll={handleScroll}
|
||||||
followOutput={'smooth'}
|
|
||||||
scrollerRef={(ref) => {
|
|
||||||
if (ref) ref.addEventListener('scroll', handleScroll)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||||
import useSWR from 'swr'
|
|
||||||
import {
|
import {
|
||||||
getBaseConfig,
|
getBaseConfig,
|
||||||
getRuleProviders,
|
getRuleProviders,
|
||||||
@ -15,10 +15,24 @@ import {
|
|||||||
getRunningMode,
|
getRunningMode,
|
||||||
getSystemProxy,
|
getSystemProxy,
|
||||||
} from '@/services/cmds'
|
} from '@/services/cmds'
|
||||||
import { SWR_DEFAULTS, SWR_MIHOMO } from '@/services/config'
|
|
||||||
|
|
||||||
import { AppDataContext, AppDataContextType } from './app-data-context'
|
import { AppDataContext, AppDataContextType } from './app-data-context'
|
||||||
|
|
||||||
|
const TQ_MIHOMO = {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
staleTime: 1500,
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 2000,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const TQ_DEFAULTS = {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
staleTime: 5000,
|
||||||
|
retry: 2,
|
||||||
|
} as const
|
||||||
|
|
||||||
// 全局数据提供者组件
|
// 全局数据提供者组件
|
||||||
export const AppDataProvider = ({
|
export const AppDataProvider = ({
|
||||||
children,
|
children,
|
||||||
@ -27,35 +41,35 @@ export const AppDataProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
|
|
||||||
const { data: proxiesData, mutate: refreshProxy } = useSWR(
|
const { data: proxiesData, refetch: refreshProxy } = useQuery({
|
||||||
'getProxies',
|
queryKey: ['getProxies'],
|
||||||
calcuProxies,
|
queryFn: calcuProxies,
|
||||||
SWR_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
)
|
})
|
||||||
|
|
||||||
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
const { data: clashConfig, refetch: refreshClashConfig } = useQuery({
|
||||||
'getClashConfig',
|
queryKey: ['getClashConfig'],
|
||||||
getBaseConfig,
|
queryFn: getBaseConfig,
|
||||||
SWR_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
)
|
})
|
||||||
|
|
||||||
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
const { data: proxyProviders, refetch: refreshProxyProviders } = useQuery({
|
||||||
'getProxyProviders',
|
queryKey: ['getProxyProviders'],
|
||||||
calcuProxyProviders,
|
queryFn: calcuProxyProviders,
|
||||||
SWR_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
)
|
})
|
||||||
|
|
||||||
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
|
const { data: ruleProviders, refetch: refreshRuleProviders } = useQuery({
|
||||||
'getRuleProviders',
|
queryKey: ['getRuleProviders'],
|
||||||
getRuleProviders,
|
queryFn: getRuleProviders,
|
||||||
SWR_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
)
|
})
|
||||||
|
|
||||||
const { data: rulesData, mutate: refreshRules } = useSWR(
|
const { data: rulesData, refetch: refreshRules } = useQuery({
|
||||||
'getRules',
|
queryKey: ['getRules'],
|
||||||
getRules,
|
queryFn: getRules,
|
||||||
SWR_MIHOMO,
|
...TQ_MIHOMO,
|
||||||
)
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastProfileId: string | null = null
|
let lastProfileId: string | null = null
|
||||||
@ -79,7 +93,7 @@ export const AppDataProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addWindowListener = (eventName: string, handler: EventListener) => {
|
const addWindowListener = (eventName: string, handler: EventListener) => {
|
||||||
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
|
// eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener
|
||||||
window.addEventListener(eventName, handler)
|
window.addEventListener(eventName, handler)
|
||||||
return () => window.removeEventListener(eventName, handler)
|
return () => window.removeEventListener(eventName, handler)
|
||||||
}
|
}
|
||||||
@ -222,22 +236,24 @@ export const AppDataProvider = ({
|
|||||||
}
|
}
|
||||||
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders])
|
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders])
|
||||||
|
|
||||||
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
|
const { data: sysproxy, refetch: refreshSysproxy } = useQuery({
|
||||||
'getSystemProxy',
|
queryKey: ['getSystemProxy'],
|
||||||
getSystemProxy,
|
queryFn: getSystemProxy,
|
||||||
SWR_DEFAULTS,
|
...TQ_DEFAULTS,
|
||||||
)
|
})
|
||||||
|
|
||||||
const { data: runningMode } = useSWR(
|
const { data: runningMode } = useQuery({
|
||||||
'getRunningMode',
|
queryKey: ['getRunningMode'],
|
||||||
getRunningMode,
|
queryFn: getRunningMode,
|
||||||
SWR_DEFAULTS,
|
...TQ_DEFAULTS,
|
||||||
)
|
})
|
||||||
|
|
||||||
const { data: uptimeData } = useSWR('appUptime', getAppUptime, {
|
const { data: uptimeData } = useQuery({
|
||||||
...SWR_DEFAULTS,
|
queryKey: ['appUptime'],
|
||||||
refreshInterval: 3000,
|
queryFn: getAppUptime,
|
||||||
errorRetryCount: 1,
|
...TQ_DEFAULTS,
|
||||||
|
refetchInterval: 3000,
|
||||||
|
retry: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提供统一的刷新方法
|
// 提供统一的刷新方法
|
||||||
|
|||||||
12
src/services/query-client.ts
Normal file
12
src/services/query-client.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 2000,
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 5000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user