[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.{yml, yaml}]\nindent_size = 4\n\n[*.{less, css}]\nindent_size = 4\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\n\nname: 🐞 Bug report\nabout: Create a report to help us improve\ntitle: \"[Bug] the title of bug report\"\nlabels: bug\nassignees: ''\n\n---\n\n#### Describe the bug\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/help_wanted.md",
    "content": "---\nname: 🥺 Help wanted\nabout: Confuse about the use of electron-vue-vite\ntitle: \"[Help] the title of help wanted report\"\nlabels: help wanted\nassignees: ''\n\n---\n\n#### Describe the problem you confuse\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- Thank you for contributing! -->\n\n### Description\n\n<!-- Please insert your description here and provide especially info about the \"what\" this PR is solving -->\n\n### What is the purpose of this pull request? <!-- (put an \"X\" next to an item) -->\n\n- [ ] Bug fix\n- [ ] New Feature\n- [ ] Documentation update\n- [ ] Other\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n    tags:\n      - v*.*.*\n    branches:\n      - main\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            arch: [ arm64, amd64 ]\n          - os: macos-latest\n            arch: [ arm64, amd64 ]\n          - os: windows-latest\n            arch: [ arm64, amd64 ]\n\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v4\n\n      - name: Git clone repo\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        env:\n          GIT_USER: ${{ secrets.GIT_USER }}\n          GIT_PASS: ${{ secrets.GIT_PASS }}\n          GIT_REPO_BASE: ${{ secrets.GIT_REPO_BASE }}\n          GIT_HOST: ${{ secrets.GIT_HOST }}\n        run: |\n          git clone -b main \"https://${GIT_USER}:${GIT_PASS}@${GIT_HOST}/${GIT_REPO_BASE}/focusany-pro.git\" code\n\n      - name: Git clone repo\n        if: runner.os == 'Windows'\n        env:\n          GIT_USER: ${{ secrets.GIT_USER }}\n          GIT_PASS: ${{ secrets.GIT_PASS }}\n          GIT_REPO_BASE: ${{ secrets.GIT_REPO_BASE }}\n          GIT_HOST: ${{ secrets.GIT_HOST }}\n        shell: pwsh\n        run: |\n          git clone -b main \"https://${env:GIT_USER}:${env:GIT_PASS}@${env:GIT_HOST}/${env:GIT_REPO_BASE}/focusany-pro.git\" code\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Build Prepare (Linux)\n        if: runner.os == 'Linux'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y build-essential pkg-config\n\n      - name: Build Prepare (macOS)\n        if: runner.os == 'macOS'\n        run: |\n          brew install python-setuptools\n\n      - name: Cert Prepare (macOS)\n        if: runner.os == 'macOS'\n        env:\n          MACOS_CERTIFICATE: ${{ secrets.CORP_MACOS_CERTIFICATE }}\n          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.CORP_MACOS_CERTIFICATE_PASSWORD }}\n        run: |\n          echo \"find-identity\"\n          security find-identity -p codesigning\n          echo \"$MACOS_CERTIFICATE\" | base64 --decode > certificate.p12\n          security create-keychain -p \"\" build.keychain\n          security import certificate.p12 -k build.keychain -P \"$MACOS_CERTIFICATE_PASSWORD\" -T /usr/bin/codesign\n          security list-keychains -s build.keychain\n          security set-keychain-settings -t 3600 -u build.keychain\n          security unlock-keychain -p \"\" build.keychain\n          echo \"find-identity\"\n          security find-identity -v -p codesigning build.keychain\n          echo \"find-identity\"\n          security find-identity -p codesigning\n          echo \"set-key-partition-list\"\n          security set-key-partition-list -S apple-tool:,apple: -s -k \"\" -l \"Mac Developer ID Application: Xi'an Yanyi Information Technology Co., Ltd\" -t private build.keychain\n          echo \"export\"\n          security export -k build.keychain -t certs -f x509 -p -o certificate.cer\n          echo \"add-trusted-cert\"\n          sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.cer\n          echo \"find-identity\"\n          security find-identity -p codesigning\n\n      - name: Install Dependencies (Linux/macOS)\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        working-directory: code\n        run: npm install\n\n      - name: Install Dependencies (Windows)\n        if: runner.os == 'Windows'\n        working-directory: code\n        shell: pwsh\n        run: npm install\n\n      - name: init ( Windows )\n        if: runner.os == 'Windows'\n        working-directory: code\n        shell: pwsh\n        run: bash ./scripts/init.sh\n\n      - name: init ( Linux/osx )\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        working-directory: code\n        run: bash ./scripts/init.sh\n\n      - name: Build Release Files\n        working-directory: code\n        run: |\n            npm run build\n        env:\n          DEBUG: \"electron-notarize:*\"\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n\n      - name: Set Build Name ( Linux / macOS )\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: |\n          DIST_FILE_NAME=${{ runner.os }}-${{ runner.arch }}-v$(date +%Y%m%d_%H%M%S)-${RANDOM}\n          echo ::add-mask::$DIST_FILE_NAME\n          echo DIST_FILE_NAME=$DIST_FILE_NAME >> $GITHUB_ENV\n\n      - name: Set Build Name ( Windows )\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: |\n          $randomNumber = Get-Random -Minimum 10000 -Maximum 99999\n          $DIST_FILE_NAME = \"Windows-X64-v$(Get-Date -Format 'yyyyMMdd_HHmmss')-$randomNumber\"\n          Write-Host \"::add-mask::$DIST_FILE_NAME\"\n          echo \"DIST_FILE_NAME=$DIST_FILE_NAME\" >> $env:GITHUB_ENV\n\n\n      - name: Rename output files (Linux/macOS)\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: |\n            find code/dist-release/ -type f -name 'FocusAnyPro-*' -exec bash -c 'f=\"{}\"; mv \"$f\" \"${f/FocusAnyPro-/FocusAny-}\"' \\;\n\n      - name: Upload Sourcemaps\n        if: runner.os == 'Linux'\n        working-directory: code\n        env:\n          GROW_URL: ${{ secrets.GROW_URL }}\n          GROW_APP_NAME: ${{ secrets.GROW_APP_NAME }}\n          GROW_ADMIN_API_TOKEN: ${{ secrets.GROW_ADMIN_API_TOKEN }}\n        run: |\n          BUILD_ID=$(node -p \"require('./dist/build.json').buildId\")\n          echo \"Uploading sourcemaps for buildId=${BUILD_ID} ...\"\n          find dist dist-electron/main -name \"*.map\" | while read f; do\n            echo \"  uploading $f\"\n            RESP=$(curl -s -X POST \"${GROW_URL}/api/admin/grow/buildUpload\" \\\n              -H \"Authorization: Bearer ${GROW_ADMIN_API_TOKEN}\" \\\n              -F \"appName=${GROW_APP_NAME}\" \\\n              -F \"buildId=${BUILD_ID}\" \\\n              -F \"file=@${f}\" \\\n              -F \"name=${f}\")\n            echo \"  response: ${RESP}\"\n          done\n          echo \"Sourcemap upload done.\"\n\n      - name: Rename output files (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: |\n            Get-ChildItem code\\dist-release\\FocusAnyPro-* | Rename-Item -NewName { $_.Name -replace 'FocusAnyPro-','FocusAny-' }\n\n\n      - name: Upload\n        if: github.event_name == 'workflow_dispatch'\n        uses: modstart/github-oss-action@master\n        with:\n          title: ${{ github.event.head_commit.message }}\n          key-id: ${{ secrets.OSS_2_KEY_ID }}\n          key-secret: ${{ secrets.OSS_2_KEY_SECRET }}\n          region: ${{ secrets.OSS_2_REGION }}\n          bucket: ${{ secrets.OSS_2_BUCKET }}\n          callbackTitle: ✅FocusAny-${{ runner.os }}-打包成功\n          callback: ${{ secrets.OSS_2_CALLBACK }}\n          assets: |\n            code/dist-release/*.exe:apps/focusany-${{ env.DIST_FILE_NAME }}/\n            code/dist-release/*.dmg:apps/focusany-${{ env.DIST_FILE_NAME }}/\n            code/dist-release/*.AppImage:apps/focusany-${{ env.DIST_FILE_NAME }}/\n            code/dist-release/*.deb:apps/focusany-${{ env.DIST_FILE_NAME }}/\n\n      - name: Release Assets\n        if: github.event_name == 'push'\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          prerelease: false\n          fail_on_unmatched_files: false\n          overwrite_files: true\n          files: |\n            code/dist-release/*.exe\n            code/dist-release/*.dmg\n            code/dist-release/*.AppImage\n            code/dist-release/*.deb\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}\n\n"
  },
  {
    "path": ".github/workflows/main-build.yml",
    "content": "name: MainBuild\n\non:\n    push:\n        branches:\n            - mainx\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n          include:\n              - os: ubuntu-latest\n                arch: [arm64, amd64]\n              - os: macos-latest\n                arch: [arm64, amd64]\n              - os: windows-latest\n                arch: [arm64, amd64]\n\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Build Prepare (Linux)\n        if: runner.os == 'Linux'\n        run: |\n            sudo apt-get update\n            sudo apt-get install -y build-essential pkg-config\n\n      - name: Build Prepare (macOS)\n        if: runner.os == 'macOS'\n        run: |\n            brew install python-setuptools\n\n      - name: Cert Prepare (macOS)\n        if: runner.os == 'macOS'\n        env:\n            MACOS_CERTIFICATE: ${{ secrets.CORP_MACOS_CERTIFICATE }}\n            MACOS_CERTIFICATE_PASSWORD: ${{ secrets.CORP_MACOS_CERTIFICATE_PASSWORD }}\n        run: |\n            echo \"find-identity\"\n            security find-identity -p codesigning\n            echo \"$MACOS_CERTIFICATE\" | base64 --decode > certificate.p12\n            security create-keychain -p \"\" build.keychain\n            security import certificate.p12 -k build.keychain -P \"$MACOS_CERTIFICATE_PASSWORD\" -T /usr/bin/codesign\n            security list-keychains -s build.keychain\n            security set-keychain-settings -t 3600 -u build.keychain\n            security unlock-keychain -p \"\" build.keychain\n            echo \"find-identity\"\n            security find-identity -v -p codesigning build.keychain\n            echo \"find-identity\"\n            security find-identity -p codesigning\n            echo \"set-key-partition-list\"\n            security set-key-partition-list -S apple-tool:,apple: -s -k \"\" -l \"Mac Developer ID Application: Xi'an Yanyi Information Technology Co., Ltd\" -t private build.keychain\n            echo \"export\"\n            security export -k build.keychain -t certs -f x509 -p -o certificate.cer\n            echo \"add-trusted-cert\"\n            sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.cer\n            echo \"find-identity\"\n            security find-identity -p codesigning\n\n      - name: Install Dependencies (Linux/macOS)\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: |\n            npm install --ignore-scripts\n            rm -rf node_modules/canvas\n            npx electron-builder install-app-deps\n\n      - name: Install Dependencies (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: |\n            npm install --ignore-scripts\n            if (Test-Path node_modules\\canvas) { Remove-Item -Recurse -Force node_modules\\canvas }\n            npx electron-builder install-app-deps\n\n      - name: init ( Windows )\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: bash ./scripts/init.sh\n\n      - name: init ( Linux/osx )\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: bash ./scripts/init.sh\n\n      - name: Build Release Files\n        run: npm run build\n        env:\n          DEBUG: \"electron-notarize:*\"\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n\n      - name: Set Build Name ( Linux / macOS )\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: |\n            DIST_FILE_NAME=${{ runner.os }}-${{ runner.arch }}-v$(date +%Y%m%d_%H%M%S)-${RANDOM}\n            echo ::add-mask::$DIST_FILE_NAME\n            echo DIST_FILE_NAME=$DIST_FILE_NAME >> $GITHUB_ENV\n\n      - name: Set Build Name ( Windows )\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: |\n            $randomNumber = Get-Random -Minimum 10000 -Maximum 99999\n            $DIST_FILE_NAME = \"Windows-X64-v$(Get-Date -Format 'yyyyMMdd_HHmmss')-$randomNumber\"\n            Write-Host \"::add-mask::$DIST_FILE_NAME\"\n            echo \"DIST_FILE_NAME=$DIST_FILE_NAME\" >> $env:GITHUB_ENV\n\n      - name: Upload\n        uses: modstart/github-oss-action@master\n        with:\n            title: ${{ github.event.head_commit.message }}\n            key-id: ${{ secrets.OSS_2_KEY_ID }}\n            key-secret: ${{ secrets.OSS_2_KEY_SECRET }}\n            region: ${{ secrets.OSS_2_REGION }}\n            bucket: ${{ secrets.OSS_2_BUCKET }}\n            callbackUrlSign: ${{ secrets.OSS_2_CALLBACK_URL_SIGN }}\n            callback: ${{ secrets.OSS_2_CALLBACK }}\n            assets: |\n                dist-release/*.exe:apps/focusany-${{ env.DIST_FILE_NAME }}/\n                dist-release/*.dmg:apps/focusany-${{ env.DIST_FILE_NAME }}/\n                dist-release/*.AppImage:apps/focusany-${{ env.DIST_FILE_NAME }}/\n                dist-release/*.deb:apps/focusany-${{ env.DIST_FILE_NAME }}/\n\n      - name: Upload Artifact Windows\n        if: runner.os == 'Windows'\n        uses: actions/upload-artifact@v4\n        with:\n            name: windows-artifact\n            path: |\n                dist-release/*.exe\n\n      - name: Upload Artifact Macos\n        if: runner.os == 'macOS'\n        uses: actions/upload-artifact@v4\n        with:\n            name: macos-artifact\n            path: |\n                dist-release/*.dmg\n\n      - name: Upload Artifact Linux\n        if: runner.os == 'Linux'\n        uses: actions/upload-artifact@v4\n        with:\n            name: linux-artifact\n            path: |\n                dist-release/*.AppImage\n                dist-release/*.deb\n\n"
  },
  {
    "path": ".github/workflows/tag-release.yml",
    "content": "name: TagRelease\n\non:\n    push:\n        tags:\n            - v*.*.*\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n          include:\n              - os: ubuntu-latest\n                arch: [arm64, amd64]\n              - os: macos-latest\n                arch: [arm64, amd64]\n              - os: windows-latest\n                arch: [arm64, amd64]\n\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Build Prepare (Linux)\n        if: runner.os == 'Linux'\n        run: |\n            sudo apt-get update\n            sudo apt-get install -y build-essential pkg-config\n\n      - name: Build Prepare (macOS)\n        if: runner.os == 'macOS'\n        run: |\n            brew install python-setuptools\n\n      - name: Cert Prepare (macOS)\n        if: runner.os == 'macOS'\n        env:\n            MACOS_CERTIFICATE: ${{ secrets.CORP_MACOS_CERTIFICATE }}\n            MACOS_CERTIFICATE_PASSWORD: ${{ secrets.CORP_MACOS_CERTIFICATE_PASSWORD }}\n        run: |\n            echo \"find-identity\"\n            security find-identity -p codesigning\n            echo \"$MACOS_CERTIFICATE\" | base64 --decode > certificate.p12\n            security create-keychain -p \"\" build.keychain\n            security import certificate.p12 -k build.keychain -P \"$MACOS_CERTIFICATE_PASSWORD\" -T /usr/bin/codesign\n            security list-keychains -s build.keychain\n            security set-keychain-settings -t 3600 -u build.keychain\n            security unlock-keychain -p \"\" build.keychain\n            echo \"find-identity\"\n            security find-identity -v -p codesigning build.keychain\n            echo \"find-identity\"\n            security find-identity -p codesigning\n            echo \"set-key-partition-list\"\n            security set-key-partition-list -S apple-tool:,apple: -s -k \"\" -l \"Mac Developer ID Application: Xi'an Yanyi Information Technology Co., Ltd\" -t private build.keychain\n            echo \"export\"\n            security export -k build.keychain -t certs -f x509 -p -o certificate.cer\n            echo \"add-trusted-cert\"\n            sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.cer\n            echo \"find-identity\"\n            security find-identity -p codesigning\n\n      - name: Install Dependencies (Linux/macOS)\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: |\n            npm install --ignore-scripts\n            rm -rf node_modules/canvas\n            npx electron-builder install-app-deps\n\n      - name: Install Dependencies (Windows)\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: |\n            npm install --ignore-scripts\n            if (Test-Path node_modules\\canvas) { Remove-Item -Recurse -Force node_modules\\canvas }\n            npx electron-builder install-app-deps\n\n      - name: init ( Windows )\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: bash ./scripts/init.sh\n\n      - name: init ( Linux/osx )\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: bash ./scripts/init.sh\n\n      - name: Build Release Files\n        run: npm run build\n        env:\n          DEBUG: \"electron-notarize:*\"\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n\n      - name: Set Build Name ( Linux / macOS )\n        if: runner.os == 'Linux' || runner.os == 'macOS'\n        run: |\n            DIST_FILE_NAME=${{ runner.os }}-${{ runner.arch }}-v$(date +%Y%m%d_%H%M%S)-${RANDOM}\n            echo ::add-mask::$DIST_FILE_NAME\n            echo DIST_FILE_NAME=$DIST_FILE_NAME >> $GITHUB_ENV\n\n      - name: Set Build Name ( Windows )\n        if: runner.os == 'Windows'\n        shell: pwsh\n        run: |\n            $randomNumber = Get-Random -Minimum 10000 -Maximum 99999\n            $DIST_FILE_NAME = \"Windows-X64-v$(Get-Date -Format 'yyyyMMdd_HHmmss')-$randomNumber\"\n            Write-Host \"::add-mask::$DIST_FILE_NAME\"\n            echo \"DIST_FILE_NAME=$DIST_FILE_NAME\" >> $env:GITHUB_ENV\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n/dist\n/dist-ssr\n/dist-electron\n/dist-release\n*.local\n\n# Editor directories and files\n.vscode/.debug.env\n.idea/\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# lockfile\npnpm-lock.yaml\nyarn.lock\ndatabase.db\n\n/focusany-plugin-*\n/data\n/data-*\n.vscode\n.github/copilot-instructions.md\n.vscode\n.github/copilot-instructions.md\n.vscode\n"
  },
  {
    "path": ".npmrc",
    "content": "# For electron-builder\n# https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422\nshamefully-hoist=true\n\n# For China 🇨🇳 developers\nelectron_mirror=https://npmmirror.com/mirrors/electron/\nelectron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/\n"
  },
  {
    "path": ".nvmrc",
    "content": "20\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of this License; and\nYou must cause any modified files to carry prominent notices stating that You changed the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.\nYou may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "Makefile",
    "content": "\n\n# make test              → 完整测试套件（构建验证）\n# make test biz          → 直接运行全部 test/biz/*.test.ts（需已启动 Electron）\n# make test ui           → 直接运行全部 test/ui/*.test.ts（需已启动 Electron）\n# make test biz xxx      → 单独运行 test/biz/xxx.test.ts\n# make test ui  xxx      → 单独运行 test/ui/xxx.test.ts\n_TEST_TYPE := $(word 2,$(MAKECMDGOALS))\n_TEST_NAME := $(word 3,$(MAKECMDGOALS))\n\n.PHONY: test biz ui dev-seed dev publish build_and_install build-cli\n\ntest:\n\t@if [ \"$(_TEST_TYPE)\" = \"biz\" ] && [ -n \"$(_TEST_NAME)\" ]; then \\\n\t\tnpx tsx test/biz/$(_TEST_NAME).test.ts; \\\n\telif [ \"$(_TEST_TYPE)\" = \"ui\" ] && [ -n \"$(_TEST_NAME)\" ]; then \\\n\t\tnpx tsx test/ui/$(_TEST_NAME).test.ts; \\\n\telif [ \"$(_TEST_TYPE)\" = \"biz\" ]; then \\\n\t\tfailed=0; \\\n\t\tfor f in test/biz/*.test.ts; do [ -f \"$$f\" ] || continue; npx tsx \"$$f\" || failed=1; done; \\\n\t\texit $$failed; \\\n\telif [ \"$(_TEST_TYPE)\" = \"ui\" ]; then \\\n\t\tfailed=0; \\\n\t\tfor f in test/ui/*.test.ts; do [ -f \"$$f\" ] || continue; npx tsx \"$$f\" || failed=1; done; \\\n\t\texit $$failed; \\\n\telse \\\n\t\tnpm run build:preview 2>&1 | tail -20; \\\n\tfi\n\nbiz ui:\n\t@:\n\ndev-seed:\n\tnpx tsx test/dev-seed.ts\n\ndev:\n\tnpm run dev:mac\n\npublish:\n\tss-publish publish ../focusany\n\tcd ../focusany && make test\n\nbuild_and_install:\n\trm -rfv dist-release/*.dmg\n\trm -rfv dist-release/*.blockmap\n\trm -rfv dist-release/*.yml\n\trm -rfv dist-release/*.yaml\n\trm -rfv dist-release/*.zip\n\tnpm run build:mac-arm\n\trm -rfv /Applications/FocusAny.app\n\tcp -a ./dist-release/mac-arm64/FocusAny.app /Applications\n\nbuild-cli:\n\t@VERSION=$$(node -p \"require('./package.json').version\") && \\\n\tmkdir -p dist-cli && \\\n\techo \"Building CLI version $$VERSION ...\" && \\\n\tcd cli && \\\n\tGOOS=darwin  GOARCH=amd64  go build -ldflags=\"-X main.Version=$$VERSION\" -o ../dist-cli/focusany-darwin-x64    . && \\\n\tGOOS=darwin  GOARCH=arm64  go build -ldflags=\"-X main.Version=$$VERSION\" -o ../dist-cli/focusany-darwin-arm64  . && \\\n\tGOOS=linux   GOARCH=amd64  go build -ldflags=\"-X main.Version=$$VERSION\" -o ../dist-cli/focusany-linux-x64     . && \\\n\tGOOS=linux   GOARCH=arm64  go build -ldflags=\"-X main.Version=$$VERSION\" -o ../dist-cli/focusany-linux-arm64   . && \\\n\tGOOS=windows GOARCH=amd64  go build -ldflags=\"-X main.Version=$$VERSION\" -o ../dist-cli/focusany-win-x64.exe   . && \\\n\tGOOS=windows GOARCH=arm64  go build -ldflags=\"-X main.Version=$$VERSION\" -o ../dist-cli/focusany-win-arm64.exe . && \\\n\techo \"CLI binaries built in dist-cli/\"\n\n# 吸收 `make test biz/ui <name>` 中任意测试名称，防止 \"No rule to make target\" 报错\n.DEFAULT:\n\t@:\n"
  },
  {
    "path": "README.md",
    "content": "# 🎯 FocusAny - 智能AI办公助理\n\n<div align=\"center\">\n  <img src=\"./screenshots/cn/home.png\" alt=\"FocusAny 主界面\" width=\"800\"/>\n</div>\n\n<div align=\"center\">\n  <img src=\"https://img.shields.io/badge/Framework-TS%2BVue3%2BElectron-blue\" alt=\"Framework\"/>\n  <a href=\"https://focusany.com\"><img src=\"https://img.shields.io/badge/WEB-focusany.com-blue\" alt=\"Website\"/></a>\n  <a href=\"https://github.com/modstart-lib/focusany\"><img src=\"https://img.shields.io/github/stars/modstart-lib/focusany.svg\" alt=\"GitHub Stars\"/></a>\n  <a href=\"https://gitee.com/modstart-lib/focusany\"><img src=\"https://gitee.com/modstart-lib/focusany/badge/star.svg\" alt=\"Gitee Stars\"/></a>\n  <a href=\"https://gitcode.com/modstart-lib/focusany\"><img src=\"https://gitcode.com/modstart-lib/focusany/star/badge.svg\" alt=\"GitCode Stars\"/></a>\n  <img src=\"https://img.shields.io/badge/License-Apache%202.0-green\" alt=\"License\"/>\n</div>\n\n## ✨ 项目简介\n\n`FocusAny` 是一个强大的智能AI办公助理，专为提升工作效率而设计。它支持市场插件和本地插件的一键启动，让你能够快速扩展功能，打造个性化的办公环境。\n\n🚀 **快速启动** | 🔧 **插件扩展** | 🎨 **现代化界面** | 🌙 **暗黑模式支持**\n\n## 📋 功能特性\n\n- ⚙️ **功能设置**：自定义呼出快捷键，开机自启动\n- 🛠️ **插件管理**：一键安装、卸载、启用/禁用插件\n- 🎯 **动作管理**：内置和插件动作快速预览和管理\n- 📁 **文件快速启动**：瞬间定位目标文件\n- ⌨️ **快捷键启动**：全局快捷键快速启动应用\n- 💾 **数据中心**：文件导出同步、WebDAV 文件同步\n- 🌙 **暗黑模式**：护眼的暗黑主题界面\n\n## 🔌 插件生态\n\nFocusAny 拥有丰富的插件生态系统，支持各种办公场景：\n\n### 插件市场概览\n\n<table width=\"100%\">\n  <thead>\n    <tr>\n      <th colspan=\"2\">🎪 插件市场</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td colspan=\"2\">\n        <img src=\"./screenshots/cn/plugin/Store.png\" alt=\"插件市场\" style=\"width:100%; border-radius: 8px;\"/>\n      </td>\n    </tr>\n    <tr>\n      <th width=\"50%\">📝 Markdown插件</th>\n      <th>🛠️ Ctool程序员工具箱</th>\n    </tr>\n    <tr>\n      <td><img src=\"./screenshots/cn/plugin/Markdown.png\" alt=\"Markdown插件\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"./screenshots/cn/plugin/Ctool.png\" alt=\"Ctool工具箱\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n    <tr>\n      <th>🌐 翻译插件</th>\n      <th>📋 剪切板插件</th>\n    </tr>\n    <tr>\n      <td><img src=\"./screenshots/cn/plugin/Translate.png\" alt=\"翻译插件\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"./screenshots/cn/plugin/Clipboard.png\" alt=\"剪切板插件\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n    <tr>\n      <th>🧠 脑图编辑器</th>\n      <th>📊 mxGraph编辑器</th>\n    </tr>\n    <tr>\n      <td><img src=\"./screenshots/cn/plugin/KityminderEditor.png\" alt=\"脑图编辑器\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"./screenshots/cn/plugin/MxgraphEditor.png\" alt=\"mxGraph编辑器\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n    <tr>\n      <th>🎨 tldraw白板</th>\n      <th>✏️ Excalidraw白板</th>\n    </tr>\n    <tr>\n      <td><img src=\"https://ms-assets.modstart.com/data/image/2024/12/27/20345_in2n_2839.png\" alt=\"tldraw白板\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"https://ms-assets.modstart.com/data/image/2024/12/23/27895_hlat_8257.png\" alt=\"Excalidraw白板\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n    <tr>\n      <th>🔐 密码管理器</th>\n      <th>🖼️ 图片美化</th>\n    </tr>\n    <tr>\n      <td><img src=\"https://ms-assets.modstart.com/data/image/2024/12/22/12047_w27p_4263.png\" alt=\"密码管理器\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"https://ms-assets.modstart.com/data/image/2024/12/22/53485_fk4f_3417.png\" alt=\"图片美化\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n    <tr>\n      <th>🔢 OTP两步验证</th>\n      <th>📸 截图与贴图</th>\n    </tr>\n    <tr>\n      <td><img src=\"https://ms-assets.modstart.com/data/image/2024/12/24/7709_81pr_6266.png\" alt=\"OTP验证\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"https://ms-assets.modstart.com/data/image/2024/12/22/42330_u3my_6770.png\" alt=\"截图工具\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n  </tbody>\n</table>\n\n💡 **持续扩展**：FocusAny 正在不断添加更多插件，让你通过插件的方式实现无限可能的功能扩展！\n\n## 🚀 快速开始\n\n### 📦 安装使用\n\n访问 [FocusAny 官网](https://focusany.com) 下载对应系统的安装包，一键安装即可开始使用！\n\n### 🛠️ 本地开发\n\n> ⚠️ 仅在 Node.js 20 环境下测试通过\n\n#### 环境准备\n\n**Ubuntu/Debian:**\n```bash\nsudo apt install -y make gcc g++ python3\n```\n\n**Windows:**\n- 安装 Visual Studio 2019，并选择 \"Desktop Development with C++\" 组件\n\n**macOS:**\n- 安装 Python 3\n\n#### 开发命令\n\n```bash\n# 安装项目依赖\nnpm install\n\n# 启动开发模式\nnpm run dev\n\n# 构建生产版本\nnpm run build\n```\n\n## 🏗️ 技术栈\n\n<div align=\"center\">\n  <img src=\"https://img.shields.io/badge/Electron-47848F?style=for-the-badge&logo=electron&logoColor=white\" alt=\"Electron\"/>\n  <img src=\"https://img.shields.io/badge/Vue.js-4FC08D?style=for-the-badge&logo=vue.js&logoColor=white\" alt=\"Vue.js\"/>\n  <img src=\"https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white\" alt=\"TypeScript\"/>\n  <img src=\"https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white\" alt=\"Node.js\"/>\n</div>\n\n## 📚 目录结构\n\n```\nfocusany/\n├── electron/          # Electron 主进程代码\n├── src/              # Vue.js 前端源码\n├── public/           # 静态资源\n├── scripts/          # 构建脚本\n├── screenshots/      # 截图资源\n└── dist-release/     # 构建输出\n```\n\n## 🤝 社区交流\n\n> 添加好友请备注 \"FocusAny\"\n\n<table width=\"100%\">\n  <thead>\n    <tr>\n      <th width=\"50%\">💬 微信交流群</th>\n      <th>🗣️ QQ交流群</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><img src=\"https://modstart.com/code_dynamic/modstart_wx\" alt=\"微信群\" style=\"width:100%; border-radius: 8px;\"/></td>\n      <td><img src=\"https://modstart.com/code_dynamic/modstart_qq\" alt=\"QQ群\" style=\"width:100%; border-radius: 8px;\"/></td>\n    </tr>\n  </tbody>\n</table>\n\n## 📄 许可证\n\n本项目采用 [Apache-2.0](LICENSE) 许可证开源。\n\n---\n\n<div align=\"center\">\n  <p>⭐ 如果这个项目对你有帮助，请给我们一个 Star！</p>\n  <p>💝 感谢所有贡献者和用户的支持</p>\n</div>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe strongly recommend always using the latest version to benefit from the latest security updates.\n\n## Reporting a Vulnerability\n\nWe take the security of our software very seriously. If you discover a security vulnerability, please follow these guidelines:\n\n### How to Report\n\nPlease **DO NOT** create a public issue for security vulnerabilities.\n\nInstead, report security vulnerabilities by emailing:\n\n📧 **Email**: `modstart@163.com`\n\n### What to Include\n\nWhen reporting a security vulnerability, please include:\n\n1. **Description**: A clear description of the vulnerability\n2. **Steps to Reproduce**: Detailed steps to reproduce the issue\n3. **Impact Assessment**: Your assessment of the potential impact\n4. **Affected Versions**: Which versions are affected\n5. **Proof of Concept**: If applicable, include a PoC or example exploit\n6. **Suggested Fix**: If you have ideas on how to fix it (optional)\n\n### Response Process\n\n- **Initial Response**: We aim to respond within 48 hours\n- **Status Updates**: We will keep you informed about the progress\n- **Disclosure Coordination**: We will coordinate with you on the disclosure timeline\n- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)\n\n### Responsible Disclosure\n\nWe ask that you:\n\n- Give us reasonable time to fix the vulnerability before public disclosure\n- Avoid exploiting the vulnerability beyond what is necessary to demonstrate it\n- Do not access, modify, or delete data belonging to others\n- Do not perform actions that could harm the availability of our services\n\n## Security Updates\n\nSecurity updates will be announced through:\n\n- GitHub Releases\n- Project Documentation\n- Email notification to users who have reported issues\n\n## Acknowledgments\n\nWe appreciate the security research community and welcome responsible disclosure of security vulnerabilities.\n\n"
  },
  {
    "path": "cli/cmd/plugin.go",
    "content": "package cmd\n\nimport (\n\t\"focusany-cli/internal\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar pluginCmd = &cobra.Command{\n\tUse:   \"plugin\",\n\tShort: \"Manage plugins\",\n}\n\nvar pluginListCmd = &cobra.Command{\n\tUse:   \"list\",\n\tShort: \"List all installed plugins\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcfg, err := internal.LoadAuthConfig()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tresult, err := internal.DoRequest(cfg, \"GET\", \"/api/plugin/list\", nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn internal.PrintJSON(result)\n\t},\n}\n\nfunc init() {\n\tpluginCmd.AddCommand(pluginListCmd)\n}\n"
  },
  {
    "path": "cli/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar appVersion string\n\n// Execute sets the version and runs the root command.\nfunc Execute(version string) {\n\tappVersion = version\n\tif err := rootCmd.Execute(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nvar rootCmd = &cobra.Command{\n\tUse:   \"focusany\",\n\tShort: \"FocusAny CLI\",\n\tLong:  \"FocusAny command-line tool for interacting with the local FocusAny service.\",\n}\n\nfunc init() {\n\trootCmd.AddCommand(versionCmd)\n\trootCmd.AddCommand(pluginCmd)\n}\n"
  },
  {
    "path": "cli/cmd/version.go",
    "content": "package cmd\n\nimport (\n\t\"focusany-cli/internal\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar versionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Print version information\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn internal.PrintJSON(map[string]string{\n\t\t\t\"version\": appVersion,\n\t\t})\n\t},\n}\n"
  },
  {
    "path": "cli/go.mod",
    "content": "module focusany-cli\n\ngo 1.22\n\nrequire github.com/spf13/cobra v1.8.0\n\nrequire (\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n)\n"
  },
  {
    "path": "cli/go.sum",
    "content": "github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "cli/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// DoRequest sends an HTTP request to the local FocusAny HTTP server.\nfunc DoRequest(cfg *AuthConfig, method string, urlPath string, body any) (map[string]any, error) {\n\tvar reqBody io.Reader\n\tif body != nil {\n\t\tb, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request body: %w\", err)\n\t\t}\n\t\treqBody = bytes.NewReader(b)\n\t}\n\n\turl := fmt.Sprintf(\"http://127.0.0.1:%d%s\", cfg.Port, urlPath)\n\treq, err := http.NewRequest(method, url, reqBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\tif body != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+cfg.Token)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w (is FocusAny running?)\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", err)\n\t}\n\n\tif resp.StatusCode == http.StatusUnauthorized {\n\t\treturn nil, fmt.Errorf(\"unauthorized: token mismatch, restart FocusAny and try again\")\n\t}\n\n\tvar result map[string]any\n\tif err := json.Unmarshal(respBytes, &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse response: %w\", err)\n\t}\n\treturn result, nil\n}\n\n// PrintJSON outputs a value as indented JSON to stdout.\nfunc PrintJSON(v any) error {\n\tb, err := json.MarshalIndent(v, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tfmt.Println(string(b))\n\treturn nil\n}\n"
  },
  {
    "path": "cli/internal/config.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n)\n\n// AuthConfig holds the port and token read from cli-auth.json\ntype AuthConfig struct {\n\tPort  int    `json:\"port\"`\n\tToken string `json:\"token\"`\n}\n\n// userDataDir returns the Electron userData directory path matching app.getPath('userData')\n// which uses the app name \"focusany\".\nfunc userDataDir() (string, error) {\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn filepath.Join(home, \"Library\", \"Application Support\", \"focusany\"), nil\n\tcase \"windows\":\n\t\tappData := os.Getenv(\"APPDATA\")\n\t\tif appData == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"APPDATA environment variable not set\")\n\t\t}\n\t\treturn filepath.Join(appData, \"focusany\"), nil\n\tdefault:\n\t\t// Linux: XDG_CONFIG_HOME or ~/.config\n\t\tconfigDir := os.Getenv(\"XDG_CONFIG_HOME\")\n\t\tif configDir == \"\" {\n\t\t\thome, err := os.UserHomeDir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tconfigDir = filepath.Join(home, \".config\")\n\t\t}\n\t\treturn filepath.Join(configDir, \"focusany\"), nil\n\t}\n}\n\n// LoadAuthConfig reads cli-auth.json from the focusany userData directory.\nfunc LoadAuthConfig() (*AuthConfig, error) {\n\tdir, err := userDataDir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot determine userData directory: %w\", err)\n\t}\n\tfilePath := filepath.Join(dir, \"cli-auth.json\")\n\tdata, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot read %s: %w (is FocusAny running?)\", filePath, err)\n\t}\n\tvar cfg AuthConfig\n\tif err := json.Unmarshal(data, &cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid cli-auth.json: %w\", err)\n\t}\n\tif cfg.Port == 0 || cfg.Token == \"\" {\n\t\treturn nil, fmt.Errorf(\"cli-auth.json is incomplete (port=%d, token empty=%v)\", cfg.Port, cfg.Token == \"\")\n\t}\n\treturn &cfg, nil\n}\n"
  },
  {
    "path": "cli/main.go",
    "content": "package main\n\nimport \"focusany-cli/cmd\"\n\n// Version is injected at build time via ldflags: -X main.Version=x.x.x\nvar Version = \"dev\"\n\nfunc main() {\n\tcmd.Execute(Version)\n}\n"
  },
  {
    "path": "electron/config/common.ts",
    "content": "export const CommonConfig = {\n    darkModeEnable: true,\n    dbSystem: \"system\",\n    dbConfigId: \"config\",\n    dbDisabledActionMatchId: \"disabledActionMatch\",\n    dbPinActionId: \"pinAction\",\n    dbFileId: \"file\",\n    dbLaunchId: \"launch\",\n    dbCustomActionId: \"customAction\",\n    dbHistoryActionId: \"historyAction\",\n    dbPluginConfigId: \"pluginConfig\",\n    dbPluginIdPrefix: \"plugin\",\n    dbPluginStorageIdPrefix: \"storage\",\n};\n"
  },
  {
    "path": "electron/config/contextMenu.ts",
    "content": "import contextMenu from \"electron-context-menu\";\n\nconst init = () => {\n    contextMenu({\n        showSaveImageAs: false,\n        showCopyLink: false,\n        showCopyImage: false,\n        showSelectAll: false,\n        showInspectElement: false,\n        showSearchWithGoogle: false,\n        showLookUpSelection: false,\n    });\n};\n\nexport const ConfigContextMenu = {\n    init,\n};\n"
  },
  {
    "path": "electron/config/icon.ts",
    "content": "import { buildResolve, extraResolve } from \"../lib/env\";\n\nexport const logoPath = buildResolve(\"logo.png\");\nexport const icoLogoPath = buildResolve(\"logo.ico\");\nexport const icnsLogoPath = buildResolve(\"logo.icns\");\n\nexport const trayPath =\n    process.platform === \"darwin\"\n        ? extraResolve(\"osx/tray/iconTemplate.png\")\n        : extraResolve(\"common/tray/icon.png\");\n"
  },
  {
    "path": "electron/config/lang.ts",
    "content": "import enUS from \"./../../src/lang/en-US.json\";\nimport zhCN from \"./../../src/lang/zh-CN.json\";\nimport { isDev } from \"../lib/env\";\nimport { ConfigMain } from \"../mapi/config/main\";\n\nexport const defaultLocale = \"zh-CN\";\n\nlet locale = defaultLocale;\n\nexport const langMessageList = [\n    {\n        name: \"en-US\",\n        label: \"English\",\n        messages: enUS,\n    },\n    {\n        name: \"zh-CN\",\n        label: \"简体中文\",\n        messages: zhCN,\n    },\n];\n\nconst buildMessages = (): any => {\n    let messages = {};\n    for (let m of langMessageList) {\n        messages[m.name] = m.messages;\n    }\n    return messages;\n};\n\nlet messages = buildMessages();\n\nexport const t = (text: string, param: object | null = null) => {\n    if (messages[locale]) {\n        if (messages[locale][text]) {\n            if (param) {\n                return messages[locale][text].replace(\n                    /\\{(\\w+)\\}/g,\n                    function (match, key) {\n                        return key in param ? param[key] : match;\n                    },\n                );\n            }\n            return messages[locale][text];\n        }\n    }\n    if (param) {\n        return text.replace(/\\{(\\w+)\\}/g, function (match, key) {\n            return key in param ? param[key] : match;\n        });\n    }\n    return text;\n};\n\nconst readyAsync = async () => {\n    locale = await ConfigMain.get(\"lang\", defaultLocale);\n};\n\nconst getLocale = () => {\n    return locale;\n};\n\nexport const ConfigLang = {\n    readyAsync,\n    getLocale,\n};\n"
  },
  {
    "path": "electron/config/menu.ts",
    "content": "import { app, Menu } from \"electron\";\nimport { isDev, isMac } from \"../lib/env\";\nimport { t } from \"./lang\";\n\nlet contextMenu: Electron.Menu;\n\nconst ready = () => {\n    const menuTemplate: Electron.MenuItemConstructorOptions[] = [];\n    if (isMac) {\n        menuTemplate.push({\n            label: app.name,\n            submenu: [\n                { label: `${t(\"menu.about\")}${app.name}`, role: \"about\" },\n                { type: \"separator\" },\n                // {\n                //     label: t(\"设置\"),\n                //     click: () => {\n                //         createSettingWindow();\n                //     },\n                //     accelerator: \"CmdOrCtrl+,\",\n                // },\n                // {type: \"separator\"},\n                { label: t(\"menu.services\"), role: \"services\" },\n                { type: \"separator\" },\n                { label: `${t(\"menu.hide\")} ${app.name}`, role: \"hide\" },\n                { label: t(\"menu.hideOthers\"), role: \"hideOthers\" },\n                { label: t(\"menu.showAll\"), role: \"unhide\" },\n                { type: \"separator\" },\n                { label: t(\"menu.quit\"), role: \"quit\" },\n            ],\n        });\n    }\n    menuTemplate.push({\n        label: t(\"menu.edit\"),\n        submenu: [\n            { label: t(\"menu.undo\"), accelerator: \"CmdOrCtrl+Z\", role: \"undo\" },\n            {\n                label: t(\"menu.redo\"),\n                accelerator: \"Shift+CmdOrCtrl+Z\",\n                role: \"redo\",\n            },\n            { type: \"separator\" },\n            { label: t(\"menu.cut\"), accelerator: \"CmdOrCtrl+X\", role: \"cut\" },\n            { label: t(\"menu.copy\"), accelerator: \"CmdOrCtrl+C\", role: \"copy\" },\n            {\n                label: t(\"menu.paste\"),\n                accelerator: \"CmdOrCtrl+V\",\n                role: \"paste\",\n            },\n            {\n                label: t(\"menu.selectAll\"),\n                accelerator: \"CmdOrCtrl+A\",\n                role: \"selectAll\",\n            },\n        ],\n    });\n    if (isDev) {\n        menuTemplate.push({\n            label: t(\"menu.view\"),\n            submenu: [\n                { label: t(\"menu.reload\"), role: \"reload\" },\n                { label: t(\"menu.forceReload\"), role: \"forceReload\" },\n                { label: t(\"menu.devTools\"), role: \"toggleDevTools\" },\n                { type: \"separator\" },\n                {\n                    label: t(\"menu.actualSize\"),\n                    role: \"resetZoom\",\n                    accelerator: \"\",\n                },\n                { label: t(\"menu.zoomIn\"), role: \"zoomIn\" },\n                { label: t(\"menu.zoomOut\"), role: \"zoomOut\" },\n                { type: \"separator\" },\n                { label: t(\"menu.fullscreen\"), role: \"togglefullscreen\" },\n            ],\n        });\n    }\n    // menuTemplate.push({\n    //     label: t(\"帮助\"),\n    //     role: \"help\",\n    //     submenu: [\n    //         // {\n    //         //     label: t(\"教程帮助\"),\n    //         //     click: () => {\n    //         //         createHelpWindow();\n    //         //     },\n    //         // },\n    //         // {type: \"separator\"},\n    //         // {\n    //         //     label: t(\"关于\"),\n    //         //     click: () => {\n    //         //         PageAbout.open().then()\n    //         //     },\n    //         // },\n    //     ],\n    // })\n    const menu = Menu.buildFromTemplate(menuTemplate);\n    Menu.setApplicationMenu(menu);\n};\n\nexport const ConfigMenu = {\n    ready,\n};\n"
  },
  {
    "path": "electron/config/tray.ts",
    "content": "import { app, Menu, shell, Tray } from \"electron\";\nimport { trayPath } from \"./icon\";\nimport { AppRuntime } from \"../mapi/env\";\nimport { AppConfig } from \"../../src/config\";\nimport { t } from \"./lang\";\nimport { isMac, isWin } from \"../lib/env\";\nimport { AppsMain } from \"../mapi/app/main\";\n\nlet tray = null;\n\nconst showApp = () => {\n    AppRuntime.mainWindow.show();\n    AppRuntime.mainWindow.focus();\n};\n\nconst hideApp = () => {\n    if (isMac) {\n        app.dock.hide();\n    }\n    AppRuntime.mainWindow.hide();\n};\n\nconst quitApp = () => {\n    (app as any).forceQuit = true;\n    app.quit();\n};\n\nconst ready = () => {\n    const contextMenu = Menu.buildFromTemplate([\n        {\n            label: t(\"tray.showMain\"),\n            click: () => {\n                showApp();\n            },\n        },\n        {\n            label: t(\"nav.guide\"),\n            click: () => {\n                AppsMain.windowOpen(\"guide\").then();\n            },\n        },\n        {\n            label: t(\"tray.visitWebsite\"),\n            click: () => {\n                shell.openExternal(AppConfig.website);\n            },\n        },\n        { type: \"separator\" },\n        {\n            label: t(\"tray.restart\"),\n            click: () => {\n                app.relaunch();\n                quitApp();\n            },\n        },\n        {\n            label: t(\"menu.quit\"),\n            click: () => {\n                quitApp();\n            },\n        },\n        { type: \"separator\" },\n        {\n            label: t(\"about.title\"),\n            click: () => {\n                AppsMain.windowOpen(\"about\").then();\n            },\n        },\n    ]);\n    tray = new Tray(trayPath);\n    tray.setToolTip(AppConfig.name);\n    tray.on(\"click\", () => {\n        showApp();\n    });\n    tray.on(\"right-click\", () => {\n        tray.popUpContextMenu(contextMenu);\n    });\n};\n\nconst show = () => {\n    if (tray) {\n        tray.destroy();\n        tray = null;\n    }\n};\n\nexport const ConfigTray = {\n    ready,\n};\n"
  },
  {
    "path": "electron/config/window.ts",
    "content": "export const WindowConfig = {\n    alwaysOpenDevTools: true,\n    minWidth: 800,\n    minHeight: 60,\n    initWidth: 800,\n    initHeight: 60,\n    mainHeight: 60,\n    mainWidth: 800,\n    mainMaxHeight: 600,\n    pluginWidth: 800,\n    pluginHeight: 500,\n    aboutWidth: 500,\n    aboutHeight: 400,\n    logWidth: 800,\n    logHeight: 600,\n    feedbackWidth: 700,\n    feedbackHeight: 600,\n    guideWidth: 800,\n    guideHeight: 540,\n    paymentWidth: 500,\n    paymentHeight: 400,\n    setupWidth: 800,\n    setupHeight: 540,\n    fastPanelWidth: 260,\n    fastPanelHeight: 500,\n    detachWindowTitleHeight: 40,\n};\n"
  },
  {
    "path": "electron/declarations/electron.d.ts",
    "content": "declare module \"electron\" {\n    interface BrowserView {\n        _window?: any;\n        _plugin?: any;\n    }\n\n    interface BrowserWindow {\n        _name?: string;\n        _plugin?: any;\n        _type?: \"action\" | \"callPage\";\n    }\n}\n"
  },
  {
    "path": "electron/declarations/svg.d.ts",
    "content": "declare module \"*.svg\" {\n    const content: string;\n    export default content;\n}\n"
  },
  {
    "path": "electron/electron-env.d.ts",
    "content": "/// <reference types=\"vite-plugin-electron/electron-env\" />\n/// <reference types=\"../sdk/focusany\" />\n\ndeclare namespace NodeJS {\n    interface ProcessEnv {\n        /**\n         * The built directory structure\n         *\n         * ```tree\n         * ├─┬ dist-electron\n         * │ ├─┬ main\n         * │ │ └── index.js    > Electron-Main\n         * │ └─┬ preload\n         * │   └── index.mjs   > Preload-Scripts\n         * ├─┬ dist\n         * │ └── index.html    > Electron-Renderer\n         * ```\n         */\n        APP_ROOT: string;\n        /** /dist/ or /public/ */\n        VITE_PUBLIC: string;\n    }\n}\n"
  },
  {
    "path": "electron/lib/api.ts",
    "content": "import Apps from \"../mapi/app\";\n\nexport type ResultType<T> = {\n    // should follow the rules:\n    // <0 business error\n    // =0 success\n    // 10000 error ( network error, server error, etc. )\n    code: number;\n    msg: string;\n    data?: T;\n};\n\nexport const post = async (url: string, data: any) => {\n    data = data || {};\n    const userAgent = Apps.getUserAgent();\n    data[\"AppManagerUserAgent\"] = userAgent;\n    return await fetch(url, {\n        method: \"POST\",\n        headers: {\n            \"User-Agent\": userAgent,\n            \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(data),\n    });\n};\n"
  },
  {
    "path": "electron/lib/devtools.ts",
    "content": "import { BrowserView, BrowserWindow, screen } from \"electron\";\nimport { isDev } from \"./env\";\nimport { WindowConfig } from \"../config/window\";\n\nexport const DevToolsManager = {\n    enable: true,\n    rowCount: 4,\n    colCount: 3,\n    windows: new Map<BrowserWindow | BrowserView, BrowserWindow>(),\n    setEnable(enable: boolean) {\n        DevToolsManager.enable = enable;\n    },\n    getWindow(win: BrowserWindow | BrowserView) {\n        return this.windows.get(win);\n    },\n    getOrCreateWindow(name: string, win: BrowserWindow | BrowserView) {\n        if (this.windows.has(win)) {\n            return this.windows.get(win);\n        }\n        const { x, y, width, height } = this.getDisplayPosition();\n        // console.log('DevToolsManager', name, {x, y, width, height})\n        const devtools = new BrowserWindow({\n            show: true,\n            x,\n            y,\n            width,\n            height,\n            title: name,\n        });\n        devtools.on(\"closed\", (e) => {\n            // console.log('DevToolsManager', 'close', name)\n            this.windows.delete(win);\n        });\n        // console.log('DevToolsManager', name, {x, y})\n        win.webContents.setDevToolsWebContents(devtools.webContents);\n        win.webContents.on(\"destroyed\", () => {\n            // console.log('DevToolsManager', 'destroyed', name)\n            devtools.destroy();\n        });\n        devtools.webContents.on(\"dom-ready\", () => {\n            setTimeout(() => {\n                if (!devtools.isDestroyed()) {\n                    devtools.setTitle(name);\n                }\n            }, 1000);\n        });\n        this.windows.set(win, devtools);\n        return devtools;\n    },\n    getLargestDisplay(): Electron.Display {\n        const displays = screen.getAllDisplays();\n        return displays.reduce((max, display) => {\n            const { width, height } = display.size;\n            const maxResolution = max.size.width * max.size.height;\n            const currentResolution = width * height;\n            return currentResolution > maxResolution ? display : max;\n        });\n    },\n    getDisplayPosition(): {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    } {\n        const display = this.getLargestDisplay();\n        const { x, y, width, height } = display.workArea;\n        // console.log('DevToolsManager', 'getDisplayPosition', {x, y, width, height})\n        if (width < 1300) {\n            this.rowCount = 3;\n            this.colCount = 2;\n        }\n        const itemWidth = Math.floor(width / this.rowCount);\n        const itemHeight = Math.floor(height / this.colCount);\n        const maxRow = Math.floor(width / itemWidth);\n        const row = this.windows.size % maxRow;\n        const col = Math.floor(this.windows.size / maxRow);\n        return {\n            x: x + row * itemWidth,\n            y: y + col * itemHeight,\n            width: itemWidth,\n            height: itemHeight,\n        };\n    },\n    register(name: string, win: BrowserWindow | BrowserView) {\n        if (!isDev || !DevToolsManager.enable) {\n            return;\n        }\n        this.getOrCreateWindow(name, win);\n    },\n    autoShow(win: BrowserWindow | BrowserView) {\n        if (!isDev || !DevToolsManager.enable) {\n            return;\n        }\n        if (WindowConfig.alwaysOpenDevTools) {\n            win.webContents.openDevTools({\n                mode: \"detach\",\n                activate: false,\n            });\n        }\n    },\n};\n"
  },
  {
    "path": "electron/lib/env-main.ts",
    "content": "import url, { fileURLToPath } from \"node:url\";\nimport { BrowserView, BrowserWindow } from \"electron\";\nimport { isPackaged } from \"./env\";\nimport path, { join } from \"node:path\";\nimport { Log } from \"../mapi/log/main\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nprocess.env.APP_ROOT = path.join(__dirname, \"../..\");\n\nexport const MAIN_DIST = path.join(process.env.APP_ROOT, \"dist-electron\");\nexport const RENDERER_DIST = path.join(process.env.APP_ROOT, \"dist\");\nexport const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;\n\nprocess.env.VITE_PUBLIC = VITE_DEV_SERVER_URL\n    ? path.join(process.env.APP_ROOT, \"public\")\n    : RENDERER_DIST;\n\nexport const preloadDefault = path.join(MAIN_DIST, \"preload/index.cjs\");\n\nexport const preloadPluginDefault = path.join(\n    MAIN_DIST,\n    \"preload-plugin/plugin.cjs\",\n);\n\nexport const rendererLoadPath = (\n    window: BrowserWindow | BrowserView,\n    fileName: string,\n) => {\n    if (!isPackaged && process.env.VITE_DEV_SERVER_URL) {\n        const x = new url.URL(rendererDistPath(fileName));\n        // console.log('rendererLoadPath', fileName, x.toString())\n        if (window instanceof BrowserView) {\n            window.webContents.loadURL(x.toString());\n        } else {\n            window.loadURL(x.toString());\n        }\n    } else {\n        // console.log('rendererLoadPath', fileName, rendererDistPath(fileName))\n        if (window instanceof BrowserView) {\n            window.webContents.loadFile(rendererDistPath(fileName));\n        } else {\n            window.loadFile(rendererDistPath(fileName));\n        }\n    }\n};\n\nexport const rendererDistPath = (fileName: string) => {\n    if (!isPackaged && process.env.VITE_DEV_SERVER_URL) {\n        return `${process.env.VITE_DEV_SERVER_URL.replace(/\\/+$/, \"\")}/${fileName}`;\n    }\n    return join(RENDERER_DIST, fileName);\n};\n\nexport const rendererIsUrl = (url: string) => {\n    return (\n        url.startsWith(\"http://\") ||\n        url.startsWith(\"https://\") ||\n        url.startsWith(\"file://\")\n    );\n};\n\nexport const getGpuInfo = async () => {\n    const list = [] as {\n        index: number;\n        name: string;\n        size: number;\n    }[];\n    try {\n        // @ts-ignore\n        const si = await import(\"systeminformation\");\n        const graphics = await si.graphics();\n        graphics.controllers.forEach((controller, index) => {\n            list.push({\n                index,\n                name: controller.model,\n                size: Math.ceil(controller.vram / 1024),\n            });\n        });\n    } catch (e) {\n        Log.error(\"getGpuInfo\", e);\n    }\n    return list;\n};\n"
  },
  {
    "path": "electron/lib/env.ts",
    "content": "import { execSync } from \"child_process\";\nimport { resolve } from \"node:path\";\nimport fs from \"node:fs\";\nimport os from \"os\";\nimport { Log } from \"../mapi/log\";\nimport FileIndex from \"../mapi/file\";\n\nexport const isPackaged = [\"true\"].includes(process.env.IS_PACKAGED);\n\nexport const isDev = !isPackaged;\n\nexport const isWin = process.platform === \"win32\";\n\nexport const isMac = process.platform === \"darwin\";\n\nexport const isLinux = process.platform === \"linux\";\n\nexport const isMain = process.type === \"browser\";\n\nexport const isRender = process.type === \"renderer\";\n\nexport const platformName = (): \"win\" | \"osx\" | \"linux\" | null => {\n    if (isWin) return \"win\";\n    if (isMac) return \"osx\";\n    if (isLinux) return \"linux\";\n    return null;\n};\n\nexport const memoryInfo = () => {\n    return {\n        total: os.totalmem(),\n        free: os.freemem(),\n    };\n};\n\nconst tryFirst = (functionList: (() => any)[]) => {\n    for (const fun of functionList) {\n        try {\n            return fun();\n        } catch (e) {}\n    }\n    return null;\n};\n\nlet platformVersionCache: string | null = null;\nexport const platformVersion = () => {\n    if (null === platformVersionCache) {\n        const functionList: any[] = [];\n        if (isWin) {\n            functionList.push(() =>\n                execSync(\"wmic os get Version\")\n                    .toString()\n                    .split(\"\\n\")[1]\n                    .trim(),\n            );\n            functionList.push(() =>\n                execSync(\n                    \"powershell -command \\\"(Get-ItemProperty 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion').ReleaseId\\\"\",\n                )\n                    .toString()\n                    .trim(),\n            );\n        } else if (isMac) {\n            functionList.push(() =>\n                execSync(\"sw_vers -productVersion\").toString().trim(),\n            );\n        } else if (isLinux) {\n            functionList.push(() =>\n                execSync(\"cat /etc/os-release | grep VERSION_ID\")\n                    .toString()\n                    .split(\"=\")[1]\n                    .trim()\n                    .replace(/\"/g, \"\"),\n            );\n        }\n        platformVersionCache = tryFirst(functionList);\n        if (!platformVersionCache) {\n            Log.error(\"env.platformVersion.error\");\n            platformVersionCache = \"0.0.0\";\n        }\n    }\n    return platformVersionCache;\n};\n\nexport const platformArch = (): \"x86\" | \"arm64\" | null => {\n    switch (os.arch()) {\n        case \"x64\":\n            return \"x86\";\n        case \"arm64\":\n            return \"arm64\";\n    }\n    return null;\n};\n\nlet platformUUIDCache: string | null = null;\nexport const platformUUID = () => {\n    if (null === platformUUIDCache) {\n        const functionList: any[] = [];\n        if (isWin) {\n            functionList.push(() =>\n                execSync(\"wmic csproduct get UUID\")\n                    .toString()\n                    .split(\"\\n\")[1]\n                    .trim(),\n            );\n            functionList.push(() =>\n                execSync(\n                    'powershell -command \"(Get-WmiObject Win32_ComputerSystemProduct).UUID\"',\n                )\n                    .toString()\n                    .trim(),\n            );\n        } else if (isMac) {\n            functionList.push(() =>\n                execSync(\"system_profiler SPHardwareDataType | grep UUID\")\n                    .toString()\n                    .split(\": \")[1]\n                    .trim(),\n            );\n        } else if (isLinux) {\n            functionList.push(() =>\n                execSync(\"cat /var/lib/dbus/machine-id\")\n                    .toString()\n                    .trim()\n                    .toUpperCase(),\n            );\n        }\n        platformUUIDCache = tryFirst(functionList);\n        if (!platformUUIDCache) {\n            Log.error(\"env.platformUUID.error\");\n            platformUUIDCache = \"000000\";\n        }\n    }\n    return platformUUIDCache;\n};\n\nexport const buildResolve = (value: string): string => {\n    return resolve(`electron/resources/build/${value}`);\n};\n\nexport const binResolve = (value: string): string => {\n    return resolve(process.resourcesPath, \"bin\", value);\n};\n\nexport const extraResolve = (filePath: string): string => {\n    const basePath = isPackaged ? process.resourcesPath : \"electron/resources\";\n    return resolve(basePath, \"extra\", filePath);\n};\n\nexport const extraResolveWithPlatform = (filePath: string): string => {\n    const dir = [platformName(), platformArch()].join(\"-\");\n    const p = [dir, filePath].join(\"/\");\n    return extraResolve(p);\n};\n\nexport const extraResolveBin = (filePath: string): string => {\n    if (isWin) {\n        if (!filePath.endsWith(\".exe\")) {\n            filePath += \".exe\";\n        }\n    }\n    const dir = [platformName(), platformArch()].join(\"-\");\n    const p = [dir, filePath].join(\"/\");\n    const binaryPath = extraResolve(p);\n    if (!fs.existsSync(binaryPath)) {\n        throw new Error(`Binary file not found: ${binaryPath}`);\n    }\n    return binaryPath;\n};\n"
  },
  {
    "path": "electron/lib/hooks.ts",
    "content": "import { BrowserWindow } from \"electron\";\n\ntype HookType = never | \"Show\" | \"Hide\";\n\nexport const executeHooks = async (\n    win: BrowserWindow,\n    hook: HookType,\n    data?: any,\n) => {\n    const evalJs = `\n    if(window.__page && window.__page.hooks && typeof window.__page.hooks.on${hook} === 'function' ) {\n        try {\n            window.__page.hooks.on${hook}(${JSON.stringify(data)});\n        } catch(e) {\n            console.log('executeHooks.on${hook}.error', e);\n        }\n    }`;\n    return win.webContents?.executeJavaScript(evalJs);\n};\n"
  },
  {
    "path": "electron/lib/permission.ts",
    "content": "import { isMac } from \"./env\";\n\nlet nodeMacPermissions = null;\nif (isMac) {\n    (async () => {\n        try {\n            nodeMacPermissions = await import(\"node-mac-permissions\");\n            nodeMacPermissions = nodeMacPermissions.default;\n            // console.log('nodeMacPermissions',nodeMacPermissions);\n        } catch (e) {}\n    })();\n}\n\nexport const Permissions = {\n    async checkAccessibilityAccess(): Promise<boolean> {\n        return new Promise((resolve, reject) => {\n            if (isMac) {\n                const status =\n                    nodeMacPermissions.getAuthStatus(\"accessibility\");\n                resolve(status === \"authorized\");\n            } else {\n                resolve(true);\n            }\n        });\n    },\n    async askAccessibilityAccess() {\n        nodeMacPermissions.askForAccessibilityAccess();\n    },\n    async checkScreenCaptureAccess(): Promise<boolean> {\n        return new Promise((resolve, reject) => {\n            if (isMac) {\n                const status = nodeMacPermissions.getAuthStatus(\"screen\");\n                resolve(status === \"authorized\");\n            } else {\n                resolve(true);\n            }\n        });\n    },\n    async askScreenCaptureAccess() {\n        nodeMacPermissions.askForScreenCaptureAccess(true);\n    },\n};\n"
  },
  {
    "path": "electron/lib/pinyin-util.ts",
    "content": "import PinyinMatch from \"pinyin-match\";\n\nexport const PinyinUtil = {\n    match(input, keywords) {\n        const index = PinyinMatch.match(input, keywords);\n        let inputMark = input;\n        let similarity = 0;\n        if (index) {\n            const indexStart = index[0];\n            const indexEnd = index[1];\n            inputMark =\n                input.substring(0, indexStart) +\n                \"<mark>\" +\n                input.substring(indexStart, indexEnd + 1) +\n                \"</mark>\" +\n                input.substring(indexEnd + 1);\n            similarity = (indexEnd - indexStart + 1) / input.length;\n        }\n        return {\n            matched: !!index,\n            inputMark,\n            similarity,\n        };\n    },\n    mark(text) {\n        return `<mark>${text}</mark>`;\n    },\n};\n"
  },
  {
    "path": "electron/lib/process.ts",
    "content": "/** 在主进程中获取关键信息存储到环境变量中，从而在预加载脚本中及渲染进程中使用 */\nimport { app } from \"electron\";\n\n/** 注意： app.isPackaged 可能被被某些方法改变所以请将该文件放到 main.js 必须位于非依赖项的顶部 */\nimport fixPath from \"fix-path\";\n\nif (process.platform === \"darwin\") {\n    fixPath();\n}\n\nprocess.env.IS_PACKAGED = String(app.isPackaged);\n\nprocess.env.DESKTOP_PATH = app.getPath(\"desktop\");\n\nprocess.env.CWD = process.cwd();\n\nexport const isDummy = false;\n"
  },
  {
    "path": "electron/lib/util.ts",
    "content": "import chardet from \"chardet\";\nimport dayjs from \"dayjs\";\nimport iconvLite from \"iconv-lite\";\nimport { Base64 } from \"js-base64\";\nimport * as crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport Showdown from \"showdown\";\nimport { Iconv } from \"iconv\";\nimport { isMac, isWin } from \"./env\";\nimport FileIndex from \"../mapi/file\";\n\nexport const sleep = (time = 1000) => {\n    return new Promise((resolve) => {\n        setTimeout(() => resolve(true), time);\n    });\n};\n\nexport const EncodeUtil = {\n    base32Alphabet: \"abcdefghijklmnopqrstuvwxyz234567\",\n    base32Encode(str: string) {\n        const buffer = Buffer.from(str, \"utf8\");\n        let bits = \"\";\n        let output = \"\";\n        // 将每个字节转为8位二进制\n        for (let i = 0; i < buffer.length; i++) {\n            const byte = buffer[i];\n            bits += byte.toString(2).padStart(8, \"0\");\n        }\n        // 每5位一组，转为 Base32 字符\n        for (let i = 0; i < bits.length; i += 5) {\n            const chunk = bits.slice(i, i + 5);\n            const paddedChunk = chunk.padEnd(5, \"0\"); // 不足5位补0\n            const index = parseInt(paddedChunk, 2);\n            output += EncodeUtil.base32Alphabet[index];\n        }\n        return output;\n    },\n    base32Decode(str: string) {\n        const base32Alphabet = \"abcdefghijklmnopqrstuvwxyz234567\";\n        let bits = \"\";\n        for (let i = 0; i < str.length; i++) {\n            const char = str[i];\n            const index = base32Alphabet.indexOf(char);\n            if (index === -1) {\n                throw new Error(\"Invalid Base32 character: \" + char);\n            }\n            bits += index.toString(2).padStart(5, \"0\");\n        }\n\n        const bytes: number[] = [];\n        for (let i = 0; i + 8 <= bits.length; i += 8) {\n            const byte = bits.slice(i, i + 8);\n            bytes.push(parseInt(byte, 2));\n        }\n\n        return Buffer.from(bytes).toString(\"utf8\");\n    },\n    base64Encode(str: string) {\n        return Base64.encode(str);\n    },\n    base64Decode(str: string) {\n        return Base64.decode(str);\n    },\n    md5(str: string) {\n        return crypto.createHash(\"md5\").update(str).digest(\"hex\");\n    },\n    aesEncode(str: string, key: string) {\n        const cipher = crypto.createCipheriv(\"aes-128-ecb\", key, \"\");\n        let crypted = cipher.update(str, \"utf8\", \"base64\");\n        crypted += cipher.final(\"base64\");\n        return crypted;\n    },\n    aesDecode(str: string, key: string) {\n        const decipher = crypto.createDecipheriv(\"aes-128-ecb\", key, \"\");\n        let dec = decipher.update(str, \"base64\", \"utf8\");\n        dec += decipher.final(\"utf8\");\n        return dec;\n    },\n    async fileXzipEncode(pathname: string): Promise<string> {\n        if (!fs.existsSync(pathname)) {\n            throw new Error(`Input file not found: ${pathname}`);\n        }\n\n        // Generate new filepath with .xzip extension\n        const basePath = pathname.substring(0, pathname.lastIndexOf(\".\"));\n        const outputPath = basePath + \".xzip\";\n\n        // Get file info\n        const fileStats = fs.statSync(pathname);\n        const fileSize = fileStats.size;\n        const fileExt = pathname.split(\".\").pop() || \"\";\n\n        // Generate random 16-character key\n        const encryptionKey = StrUtil.randomString(16);\n\n        // Create metadata\n        const filemeta = {\n            version: 1,\n            format: fileExt,\n            size: fileSize,\n            key: encryptionKey,\n        };\n\n        // Convert metadata to JSON and then base64 encode\n        const metaJson = JSON.stringify(filemeta);\n        const metaB64 = Buffer.from(metaJson, \"utf-8\").toString(\"base64\");\n        const metaLength = metaB64.length;\n\n        // Prepare encryption key\n        const keyBytes = Buffer.from(encryptionKey, \"utf-8\");\n        const keyLength = keyBytes.length;\n\n        // Stream processing: read, encrypt and write in chunks\n        const inputStream = fs.createReadStream(pathname);\n        const outputStream = fs.createWriteStream(outputPath);\n\n        // Write metadata length (4 bytes, little-endian)\n        const metaLengthBuffer = Buffer.allocUnsafe(4);\n        metaLengthBuffer.writeUInt32LE(metaLength, 0);\n        outputStream.write(metaLengthBuffer);\n\n        // Write base64 encoded metadata\n        outputStream.write(Buffer.from(metaB64, \"utf-8\"));\n\n        // Stream encrypt the file content\n        let bytesProcessed = 0;\n        return new Promise((resolve, reject) => {\n            inputStream.on(\"data\", (chunk: Buffer) => {\n                // XOR encrypt the chunk\n                const encryptedChunk = Buffer.alloc(chunk.length);\n                for (let i = 0; i < chunk.length; i++) {\n                    encryptedChunk[i] =\n                        chunk[i] ^ keyBytes[bytesProcessed % keyLength];\n                    bytesProcessed++;\n                }\n\n                // Write encrypted chunk\n                outputStream.write(encryptedChunk);\n            });\n\n            inputStream.on(\"end\", () => {\n                outputStream.end();\n                resolve(outputPath);\n            });\n\n            inputStream.on(\"error\", (error) => {\n                outputStream.destroy();\n                reject(error);\n            });\n\n            outputStream.on(\"error\", (error) => {\n                inputStream.destroy();\n                reject(error);\n            });\n        });\n    },\n    async fileXzipDecode(pathname: string): Promise<string> {\n        if (!fs.existsSync(pathname)) {\n            throw new Error(`Input file not found: ${pathname}`);\n        }\n\n        if (!pathname.endsWith(\".xzip\")) {\n            return pathname; // Not an xzip file, return as is\n        }\n\n        let outputPath = pathname.replace(/\\.xzip$/, \"\");\n\n        return new Promise((resolve, reject) => {\n            const inputStream = fs.createReadStream(pathname);\n            let metadataRead = false;\n            let filemeta: any = null;\n            let keyBytes: Buffer;\n            let bytesProcessed = 0;\n            let outputStream: fs.WriteStream;\n            let remainingMetaBytes = 0;\n            let metaBuffer = Buffer.alloc(0);\n\n            inputStream.on(\"data\", (chunk: Buffer) => {\n                let chunkOffset = 0;\n\n                if (!metadataRead) {\n                    if (remainingMetaBytes === 0) {\n                        // Read metadata length (first 4 bytes)\n                        if (chunk.length < 4) {\n                            reject(\n                                new Error(\n                                    \"Invalid xzip file: insufficient data for metadata length\",\n                                ),\n                            );\n                            return;\n                        }\n                        const metaLength = chunk.readUInt32LE(0);\n                        remainingMetaBytes = metaLength;\n                        chunkOffset = 4;\n                    }\n\n                    // Read metadata\n                    const availableMetaBytes = Math.min(\n                        remainingMetaBytes,\n                        chunk.length - chunkOffset,\n                    );\n                    const metaChunk = chunk.subarray(\n                        chunkOffset,\n                        chunkOffset + availableMetaBytes,\n                    );\n                    metaBuffer = Buffer.concat([\n                        metaBuffer,\n                        metaChunk,\n                    ] as readonly Uint8Array[]);\n                    remainingMetaBytes -= availableMetaBytes;\n                    chunkOffset += availableMetaBytes;\n\n                    if (remainingMetaBytes === 0) {\n                        // Parse metadata\n                        try {\n                            const metaB64 = metaBuffer.toString(\"utf-8\");\n                            const metaJson = Buffer.from(\n                                metaB64,\n                                \"base64\",\n                            ).toString(\"utf-8\");\n                            filemeta = JSON.parse(metaJson);\n                            keyBytes = Buffer.from(filemeta.key, \"utf-8\");\n\n                            // Create output file with correct extension\n                            const finalOutputPath =\n                                outputPath +\n                                (filemeta.format ? \".\" + filemeta.format : \"\");\n                            outputStream =\n                                fs.createWriteStream(finalOutputPath);\n\n                            metadataRead = true;\n\n                            // Set the final output path for resolution\n                            outputPath = finalOutputPath;\n                        } catch (error) {\n                            reject(\n                                new Error(\n                                    \"Invalid xzip file: corrupted metadata\",\n                                ),\n                            );\n                            return;\n                        }\n                    }\n                }\n\n                if (metadataRead && chunkOffset < chunk.length) {\n                    // Decrypt remaining chunk data\n                    const encryptedChunk = chunk.subarray(chunkOffset);\n                    const decryptedChunk = Buffer.alloc(encryptedChunk.length);\n                    const keyLength = keyBytes.length;\n\n                    for (let i = 0; i < encryptedChunk.length; i++) {\n                        decryptedChunk[i] =\n                            encryptedChunk[i] ^\n                            keyBytes[bytesProcessed % keyLength];\n                        bytesProcessed++;\n                    }\n\n                    outputStream.write(decryptedChunk);\n                }\n            });\n\n            inputStream.on(\"end\", () => {\n                if (outputStream) {\n                    outputStream.end();\n                    resolve(outputPath);\n                } else {\n                    reject(new Error(\"Invalid xzip file: incomplete metadata\"));\n                }\n            });\n\n            inputStream.on(\"error\", (error) => {\n                if (outputStream) {\n                    outputStream.destroy();\n                }\n                reject(error);\n            });\n\n            if (outputStream) {\n                outputStream.on(\"error\", (error) => {\n                    inputStream.destroy();\n                    reject(error);\n                });\n            }\n        });\n    },\n};\n\nexport const IconvUtil = {\n    convert(str: string, to?: string, from?: string) {\n        if (!from) {\n            from = chardet.detect(Buffer.from(str));\n        }\n        to = to || \"utf8\";\n        const buffer = iconvLite.encode(str, from);\n        return iconvLite.decode(buffer, to);\n    },\n    bufferToUtf8(buffer: Buffer) {\n        const encoding = chardet.detect(buffer);\n        // console.log('bufferToUtf8.encoding', encoding)\n        if (\"ISO-2022-CN\" === encoding) {\n            const iconvInstance = new Iconv(\n                \"ISO-2022-CN\",\n                \"UTF-8//TRANSLIT//IGNORE\",\n            );\n            return iconvInstance.convert(buffer).toString();\n        }\n        return iconvLite.decode(buffer, encoding).toString();\n    },\n    detect(buffer: Uint8Array) {\n        // detect str encoding\n        return chardet.detect(buffer);\n    },\n};\n\nexport const StrUtil = {\n    randomString(len: number = 32) {\n        const chars =\n            \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n        let result = \"\";\n        for (let i = len; i > 0; --i) {\n            result += chars[Math.floor(Math.random() * chars.length)];\n        }\n        return result;\n    },\n    uuid() {\n        return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(\n            /[xy]/g,\n            function (c) {\n                const r = (Math.random() * 16) | 0;\n                const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n                return v.toString(16);\n            },\n        );\n    },\n    hashCode(str: string, length: number = 8) {\n        let hash = 0;\n        if (str.length === 0) return hash + \"\";\n        for (let i = 0; i < str.length; i++) {\n            const char = str.charCodeAt(i);\n            hash = (hash << 5) - hash + char;\n            hash = hash & hash;\n        }\n        let result = Math.abs(hash).toString(16);\n        if (result.length < length) {\n            result = \"0\".repeat(length - result.length) + result;\n        }\n        return result;\n    },\n    hashCodeWithDuplicateCheck(\n        str: string,\n        check: string[],\n        length: number = 8,\n    ) {\n        let code = this.hashCode(str, length);\n        while (check.includes(code)) {\n            code = this.uuid().substring(0, length);\n        }\n        return code;\n    },\n    bigIntegerId() {\n        return [\n            Date.now(),\n            (Math.floor(Math.random() * 1000000) + \"\").padStart(6, \"0\"),\n        ].join(\"\");\n    },\n    ucFirst(str: string) {\n        if (!str) return \"\";\n        return str.charAt(0).toUpperCase() + str.slice(1);\n    },\n};\n\nexport const TimeUtil = {\n    timestampInMs() {\n        return Date.now();\n    },\n    timestamp() {\n        return Math.floor(Date.now() / 1000);\n    },\n    format(time: number, format: string = \"YYYY-MM-DD HH:mm:ss\") {\n        return dayjs(time).format(format);\n    },\n    formatDate(time: number) {\n        return dayjs(time).format(\"YYYY-MM-DD\");\n    },\n    dateString() {\n        return dayjs().format(\"YYYYMMDD\");\n    },\n    datetimeString() {\n        return dayjs().format(\"YYYYMMDD_HHmmss_SSS\");\n    },\n    timestampDayStart(msTimestamp?: number) {\n        let date = msTimestamp ? new Date(msTimestamp) : new Date();\n        date.setHours(0, 0, 0, 0);\n        return Math.floor(date.getTime() / 1000);\n    },\n    replacePattern(text: string) {\n        // @ts-ignore\n        return text\n            .replaceAll(\"{year}\", dayjs().format(\"YYYY\"))\n            .replaceAll(\"{month}\", dayjs().format(\"MM\"))\n            .replaceAll(\"{day}\", dayjs().format(\"DD\"))\n            .replaceAll(\"{hour}\", dayjs().format(\"HH\"))\n            .replaceAll(\"{minute}\", dayjs().format(\"mm\"))\n            .replaceAll(\"{second}\", dayjs().format(\"ss\"));\n    },\n};\n\nexport const FileUtil = {\n    MIME_TYPES: {\n        html: \"text/html\",\n        htm: \"text/html\",\n        js: \"application/javascript\",\n        css: \"text/css\",\n        json: \"application/json\",\n        png: \"image/png\",\n        jpg: \"image/jpeg\",\n        jpeg: \"image/jpeg\",\n        gif: \"image/gif\",\n        svg: \"image/svg+xml\",\n        webp: \"image/webp\",\n        woff: \"font/woff\",\n        woff2: \"font/woff2\",\n        ttf: \"font/ttf\",\n        otf: \"font/otf\",\n        mp3: \"audio/mpeg\",\n        mp4: \"video/mp4\",\n        wav: \"audio/wav\",\n        wasm: \"application/wasm\",\n        eot: \"application/vnd.ms-fontobject\",\n    },\n    getMimeByExt(ext: string, defaultMime: string = \"\"): string {\n        ext = ext.toLowerCase();\n        if (ext.startsWith(\".\")) {\n            ext = ext.substring(1);\n        }\n        return FileUtil.MIME_TYPES[ext] || defaultMime;\n    },\n    getMimeByPath(p: string, defaultMime: string = \"\"): string {\n        const extension = p.split(\".\").pop().toLowerCase();\n        return FileUtil.getMimeByExt(extension, defaultMime);\n    },\n    streamToBase64(stream: NodeJS.ReadableStream): Promise<string> {\n        return new Promise((resolve, reject) => {\n            const chunks = [];\n            stream.on(\"data\", (chunk) => {\n                chunks.push(chunk);\n            });\n            stream.on(\"end\", () => {\n                const buffer = Buffer.concat(chunks);\n                resolve(buffer.toString(\"base64\"));\n            });\n            stream.on(\"error\", (error) => {\n                reject(error);\n            });\n        });\n    },\n    bufferToBase64(buffer: Buffer) {\n        let binary = \"\";\n        let bytes = new Uint8Array(buffer);\n        let len = bytes.byteLength;\n        for (let i = 0; i < len; i++) {\n            binary += String.fromCharCode(bytes[i]);\n        }\n        return EncodeUtil.base64Encode(binary);\n    },\n    base64ToBuffer(base64: string): Buffer {\n        if (base64.startsWith(\"data:\")) {\n            base64 = base64.split(\"base64,\")[1];\n        }\n        return Buffer.from(base64, \"base64\");\n    },\n    formatSize(size: number) {\n        if (size < 1024) {\n            return size + \"B\";\n        } else if (size < 1024 * 1024) {\n            return (size / 1024).toFixed(2) + \"KB\";\n        } else if (size < 1024 * 1024 * 1024) {\n            return (size / 1024 / 1024).toFixed(2) + \"MB\";\n        } else {\n            return (size / 1024 / 1024 / 1024).toFixed(2) + \"GB\";\n        }\n    },\n    async md5(filePath: string) {\n        return new Promise((resolve, reject) => {\n            const hash = crypto.createHash(\"md5\");\n            const stream = fs.createReadStream(filePath);\n            stream.on(\"data\", (data) => {\n                hash.update(data);\n            });\n            stream.on(\"end\", () => {\n                resolve(hash.digest(\"hex\"));\n            });\n            stream.on(\"error\", (error) => {\n                reject(error);\n            });\n        });\n    },\n};\n\nexport const JsonUtil = {\n    stringifyOrdered(obj: any) {\n        return JSON.stringify(obj, Object.keys(obj).sort(), 4);\n    },\n    stringifyValueOrdered(obj: any) {\n        const sortedData = Object.fromEntries(\n            Object.entries(obj).sort(([, a], [, b]) => {\n                // @ts-ignore\n                return ((a as any) - b) as any;\n            }),\n        );\n        return JSON.stringify(sortedData, null, 4);\n    },\n};\n\nexport const ImportUtil = {\n    async loadCommonJs(cjsPath: string, forceReload: boolean = true) {\n        let tempPath = cjsPath;\n        if (forceReload) {\n            const md5 = await FileUtil.md5(cjsPath);\n            tempPath = path.join(\n                await FileIndex.tempDir(\"commonJs\"),\n                `${md5}.cjs`,\n            );\n            if (!fs.existsSync(tempPath)) {\n                fs.copyFileSync(cjsPath, tempPath);\n            }\n        }\n        const backend = await import(/* @vite-ignore */ `file://${tempPath}`);\n        // console.log('loadCommonJs', `file://${cjsPath}?t=${md5}`)\n        return backend.default;\n    },\n};\n\nexport const MemoryCacheUtil = {\n    pool: {} as {\n        [key: string]: {\n            value: any;\n            expire: number;\n        };\n    },\n    _gc() {\n        const now = TimeUtil.timestamp();\n        for (const key in this.pool) {\n            if (this.pool[key].expire < now) {\n                delete this.pool[key];\n            }\n        }\n    },\n    async remember<T extends any>(\n        key: string,\n        callback: () => Promise<any>,\n        ttl: number = 3600,\n    ) {\n        if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {\n            return this.pool[key].value as T;\n        }\n        const value = await callback();\n        this.pool[key] = {\n            value,\n            expire: TimeUtil.timestamp() + ttl,\n        };\n        this._gc();\n        return value as T;\n    },\n    get(key: string) {\n        if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {\n            return this.pool[key].value;\n        }\n        this._gc();\n        return null;\n    },\n    set(key: string, value: any, ttl: number = 86400) {\n        this.pool[key] = {\n            value,\n            expire: TimeUtil.timestamp() + ttl,\n        };\n        this._gc();\n    },\n    forget(key: string) {\n        delete this.pool[key];\n    },\n};\n\nexport const MemoryMapCacheUtil = {\n    pool: {} as {\n        [group: string]: {\n            [key: string]: {\n                value: any;\n                expire: number;\n            };\n        };\n    },\n    _gc() {\n        const now = TimeUtil.timestamp();\n        for (const group in this.pool) {\n            for (const key in this.pool[group]) {\n                if (this.pool[group][key].expire < now) {\n                    delete this.pool[group][key];\n                }\n            }\n        }\n    },\n    get(group: string, key: string) {\n        if (\n            this.pool[group] &&\n            this.pool[group][key] &&\n            this.pool[group][key].expire > TimeUtil.timestamp()\n        ) {\n            return this.pool[group][key].value;\n        }\n        this._gc();\n        return null;\n    },\n    set(group: string, key: string, value: any, ttl: number = 86400) {\n        if (!this.pool[group]) {\n            this.pool[group] = {};\n        }\n        this.pool[group][key] = {\n            value,\n            expire: TimeUtil.timestamp() + ttl,\n        };\n        this._gc();\n    },\n    forget(group: string, key: string) {\n        if (this.pool[group]) {\n            delete this.pool[group][key];\n        }\n    },\n};\n\nexport const ShellUtil = {\n    quotaPath(p: string) {\n        return `\"${p}\"`;\n    },\n    parseCommandArgs(command: string) {\n        let args = [];\n        let arg = \"\";\n        let quote = \"\";\n        let escape = false;\n        for (let i = 0; i < command.length; i++) {\n            const c = command[i];\n            if (escape) {\n                arg += c;\n                escape = false;\n                continue;\n            }\n            if (\"\\\\\" === c) {\n                escape = true;\n                arg += c;\n                continue;\n            }\n            if (\"\" === quote && (\" \" === c || \"\\t\" === c)) {\n                if (arg) {\n                    args.push(arg);\n                    arg = \"\";\n                }\n                continue;\n            }\n            if (\"\" === quote && ('\"' === c || \"'\" === c)) {\n                quote = c;\n                arg += c;\n                continue;\n            }\n            if ('\"' === quote && '\"' === c) {\n                quote = \"\";\n                arg += c;\n                continue;\n            }\n            if (\"'\" === quote && \"'\" === c) {\n                quote = \"\";\n                arg += c;\n                continue;\n            }\n            arg += c;\n        }\n        if (arg) {\n            args.push(arg);\n        }\n        return args;\n    },\n};\n\nexport const VersionUtil = {\n    /**\n     * 检测版本是否匹配\n     * @param v string\n     * @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0\n     */\n    match(v: string, match: string) {\n        if (match === \"*\") {\n            return true;\n        }\n        if (match.startsWith(\">=\")) {\n            if (this.ge(v, match.substring(2))) {\n                return true;\n            }\n        } else if (match.startsWith(\"<=\")) {\n            if (this.le(v, match.substring(2))) {\n                return true;\n            }\n        } else if (match.startsWith(\">\")) {\n            if (this.gt(v, match.substring(1))) {\n                return true;\n            }\n        } else if (match.startsWith(\"<\")) {\n            if (this.lt(v, match.substring(1))) {\n                return true;\n            }\n        } else {\n            return this.eq(v, match);\n        }\n        return false;\n    },\n    compare(v1: string, v2: string) {\n        const v1Arr = v1.split(\".\");\n        const v2Arr = v2.split(\".\");\n        for (let i = 0; i < v1Arr.length; i++) {\n            const v1Num = parseInt(v1Arr[i]);\n            const v2Num = parseInt(v2Arr[i]);\n            if (v1Num > v2Num) {\n                return 1;\n            } else if (v1Num < v2Num) {\n                return -1;\n            }\n        }\n        return 0;\n    },\n    gt(v1: string, v2: string) {\n        return VersionUtil.compare(v1, v2) > 0;\n    },\n    ge(v1: string, v2: string) {\n        return VersionUtil.compare(v1, v2) >= 0;\n    },\n    lt(v1: string, v2: string) {\n        return VersionUtil.compare(v1, v2) < 0;\n    },\n    le: (v1: string, v2: string) => {\n        return VersionUtil.compare(v1, v2) <= 0;\n    },\n    eq: (v1: string, v2: string) => {\n        return VersionUtil.compare(v1, v2) === 0;\n    },\n};\n\nexport const UIUtil = {\n    sizeToPx(size: string, sizeFull: number) {\n        if (/^\\d+$/.test(size)) {\n            // 纯数字\n            return parseInt(size);\n        } else if (size.endsWith(\"%\")) {\n            // 百分比\n            let result = Math.floor((sizeFull * parseInt(size)) / 100);\n            result = Math.min(result, sizeFull);\n            return result;\n        } else {\n            throw \"UnsupportSizeString\";\n        }\n    },\n};\n\nexport const ReUtil = {\n    match(regex: string, text: string) {\n        if (\"\" === regex || null === regex) {\n            return false;\n        }\n        if (regex.startsWith(\"/\")) {\n            const index = regex.lastIndexOf(\"/\");\n            const source = regex.slice(1, index);\n            const flags = regex.slice(index + 1);\n            return new RegExp(source, flags).test(text);\n        }\n        return new RegExp(regex).test(text);\n    },\n};\n\nconst converter = new Showdown.Converter({\n    tables: true,\n});\nexport const MarkdownUtil = {\n    toHtml(markdown: string): string {\n        return converter.makeHtml(markdown);\n    },\n};\n\ntype HotkeyModifierType =\n    | \"Control\"\n    | \"Option\"\n    | \"Command\"\n    | \"Ctrl\"\n    | \"Alt\"\n    | \"Win\"\n    | \"Meta\"\n    | \"Shift\";\ntype HotkeyType = { key: string; modifiers: HotkeyModifierType[] };\n\nexport const HotKeyUtil = {\n    orderModifiers(modifiers: HotkeyModifierType[]) {\n        const order = [\n            \"Control\",\n            \"Ctrl\",\n            \"Command\",\n            \"Meta\",\n            \"Win\",\n            \"Option\",\n            \"Alt\",\n            \"Shift\",\n        ];\n        return modifiers.sort((a, b) => {\n            return order.indexOf(a) - order.indexOf(b);\n        });\n    },\n    unifyObject(hotkey: HotkeyType) {\n        return {\n            key: hotkey.key.toUpperCase(),\n            modifiers: this.orderModifiers(\n                hotkey.modifiers.map((modifier) => StrUtil.ucFirst(modifier)),\n            ),\n        };\n    },\n    unifyString(hotkey: string): HotkeyType {\n        const parts = hotkey.split(\"+\");\n        const key = parts.pop() || \"\";\n        const modifiers: any[] = [];\n        parts.forEach((part) => {\n            modifiers.push(StrUtil.ucFirst(part.trim()));\n        });\n        return this.unifyObject({ key, modifiers });\n    },\n    unify(\n        hotkeys: string | string[] | HotkeyType | HotkeyType[],\n    ): HotkeyType[] {\n        if (typeof hotkeys === \"string\") {\n            return [this.unifyString(hotkeys)];\n        } else if (Array.isArray(hotkeys)) {\n            return hotkeys.map((hotkey) => {\n                if (typeof hotkey === \"string\") {\n                    return this.unifyString(hotkey);\n                } else {\n                    return this.unifyObject(hotkey);\n                }\n            });\n        } else {\n            return [this.unifyObject(hotkeys)];\n        }\n    },\n    getFromEvent(event: any): HotkeyType | null {\n        const valid = [\n            \"A\",\n            \"B\",\n            \"C\",\n            \"D\",\n            \"E\",\n            \"F\",\n            \"G\",\n            \"H\",\n            \"I\",\n            \"J\",\n            \"K\",\n            \"L\",\n            \"M\",\n            \"N\",\n            \"O\",\n            \"P\",\n            \"Q\",\n            \"R\",\n            \"S\",\n            \"T\",\n            \"U\",\n            \"V\",\n            \"W\",\n            \"X\",\n            \"Y\",\n            \"Z\",\n            \"1\",\n            \"2\",\n            \"3\",\n            \"4\",\n            \"5\",\n            \"6\",\n            \"7\",\n            \"8\",\n            \"9\",\n            \"0\",\n            \"Space\",\n        ];\n        const key = (event.key || \"\").toUpperCase();\n        if (!event || !event.key || !valid.includes(key)) {\n            return null;\n        }\n        const modifiers: HotkeyModifierType[] = [];\n        if (isWin) {\n            if (event.ctrlKey || event.control) {\n                modifiers.push(\"Ctrl\");\n            }\n            if (event.altKey || event.alt) {\n                modifiers.push(\"Alt\");\n            }\n            if (event.metaKey || event.meta) {\n                modifiers.push(\"Win\");\n            }\n        } else if (isMac) {\n            if (event.ctrlKey || event.control) {\n                modifiers.push(\"Control\");\n            }\n            if (event.altKey || event.alt) {\n                modifiers.push(\"Option\");\n            }\n            if (event.metaKey || event.meta) {\n                modifiers.push(\"Command\");\n            }\n        } else {\n            if (event.ctrlKey || event.control) {\n                modifiers.push(\"Ctrl\");\n            }\n            if (event.altKey || event.alt) {\n                modifiers.push(\"Alt\");\n            }\n            if (event.metaKey || event.meta) {\n                modifiers.push(\"Meta\");\n            }\n        }\n        if (event.shiftKey || event.shift) {\n            modifiers.push(\"Shift\");\n        }\n        return this.unifyObject({ key, modifiers });\n    },\n    match(hotkeysForMatch: HotkeyType[], hotkey: HotkeyType): boolean {\n        if (!hotkeysForMatch || !hotkey) {\n            return false;\n        }\n        const hotKeyStr = hotkey.modifiers.join(\"+\") + \"+\" + hotkey.key;\n        for (const key of hotkeysForMatch) {\n            const keyStr = key.modifiers.join(\"+\") + \"+\" + key.key;\n            if (keyStr === hotKeyStr) {\n                return true;\n            }\n        }\n        return false;\n    },\n};\n"
  },
  {
    "path": "electron/main/fastPanel.ts",
    "content": "import { icnsLogoPath, icoLogoPath, logoPath } from \"../config/icon\";\nimport { AppRuntime } from \"../mapi/env\";\nimport { AppConfig } from \"../../src/config\";\nimport { isPackaged } from \"../lib/env\";\nimport { WindowConfig } from \"../config/window\";\nimport {\n    preloadDefault,\n    RENDERER_DIST,\n    rendererLoadPath,\n    VITE_DEV_SERVER_URL,\n} from \"../lib/env-main\";\nimport * as remoteMain from \"@electron/remote/main\";\nimport { Page } from \"../page\";\nimport { BrowserWindow } from \"electron\";\nimport path from \"node:path\";\nimport { executeHooks } from \"../mapi/manager/lib/hooks\";\nimport { DevToolsManager } from \"../lib/devtools\";\nimport ConfigMain from \"../mapi/config/main\";\n\nexport const FastPanelMain = {\n    init() {\n        const fastPanelHtml = path.join(RENDERER_DIST, \"page/fastPanel.html\");\n        let icon = logoPath;\n        if (process.platform === \"win32\") {\n            icon = icoLogoPath;\n        } else if (process.platform === \"darwin\") {\n            icon = icnsLogoPath;\n        }\n        AppRuntime.fastPanelWindow = new BrowserWindow({\n            show: false,\n            title: AppConfig.name,\n            ...(!isPackaged ? { icon } : {}),\n            frame: false,\n            transparent: false,\n            hasShadow: true,\n            center: true,\n            useContentSize: true,\n            minWidth: WindowConfig.fastPanelWidth,\n            minHeight: WindowConfig.fastPanelHeight,\n            width: WindowConfig.fastPanelWidth,\n            height: WindowConfig.fastPanelHeight,\n            skipTaskbar: true,\n            resizable: false,\n            maximizable: false,\n            backgroundColor: \"#f1f5f9\",\n            alwaysOnTop: true,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                contextIsolation: false,\n                sandbox: false,\n                webSecurity: false,\n                webviewTag: true,\n            },\n        });\n\n        AppRuntime.fastPanelWindow.on(\"closed\", () => {\n            AppRuntime.fastPanelWindow = null;\n        });\n        AppRuntime.fastPanelWindow.on(\"show\", async () => {\n            await executeHooks(AppRuntime.fastPanelWindow, \"Show\");\n        });\n        AppRuntime.fastPanelWindow.on(\"hide\", async () => {\n            await executeHooks(AppRuntime.fastPanelWindow, \"Hide\");\n        });\n        AppRuntime.fastPanelWindow.on(\"blur\", async () => {\n            const fastPanelAutoHide = await ConfigMain.getEnv(\n                \"fastPanelAutoHide\",\n                true,\n            );\n            if (fastPanelAutoHide) {\n                AppRuntime.fastPanelWindow.hide();\n            }\n        });\n\n        rendererLoadPath(AppRuntime.fastPanelWindow, \"page/fastPanel.html\");\n\n        remoteMain.enable(AppRuntime.fastPanelWindow.webContents);\n\n        AppRuntime.fastPanelWindow.webContents.on(\"did-finish-load\", () => {\n            Page.ready(\"fastPanel\");\n            DevToolsManager.autoShow(AppRuntime.fastPanelWindow);\n        });\n        DevToolsManager.register(\"FastPanel\", AppRuntime.fastPanelWindow);\n        // AppRuntime.fastPanelWindow.webContents.setWindowOpenHandler(({url}) => {\n        //     if (url.startsWith('https:')) shell.openExternal(url)\n        //     return {action: 'deny'}\n        // })\n    },\n};\n"
  },
  {
    "path": "electron/main/index.ts",
    "content": "import { app, BrowserWindow, desktopCapturer, session, shell } from \"electron\";\nimport { optimizer } from \"@electron-toolkit/utils\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\n/** process.js 必须位于非依赖项的顶部 */\nimport { isDummy } from \"../lib/process\";\nimport * as remoteMain from \"@electron/remote/main\";\n\nimport { AppEnv, AppRuntime } from \"../mapi/env\";\nimport { MAPI } from \"../mapi/main\";\n\nimport { WindowConfig } from \"../config/window\";\nimport { AppConfig } from \"../../src/config\";\nimport Log from \"../mapi/log/main\";\nimport { ConfigMenu } from \"../config/menu\";\nimport { ConfigLang } from \"../config/lang\";\nimport { ConfigContextMenu } from \"../config/contextMenu\";\nimport { preloadDefault, rendererLoadPath } from \"../lib/env-main\";\nimport { Page } from \"../page\";\nimport { ConfigTray } from \"../config/tray\";\nimport { icnsLogoPath, icoLogoPath, logoPath } from \"../config/icon\";\nimport { isMac, isPackaged } from \"../lib/env\";\nimport { FastPanelMain } from \"./fastPanel\";\nimport { executeHooks } from \"../mapi/manager/lib/hooks\";\nimport { AppPosition } from \"../mapi/app/lib/position\";\nimport { DevToolsManager } from \"../lib/devtools\";\nimport { reportError } from \"../mapi/log/beacon\";\nimport { AppsMain } from \"../mapi/app/main\";\nimport { ManagerEditor } from \"../mapi/manager/editor\";\nimport { ProtocolMain } from \"../mapi/protocol/main\";\n\napp.commandLine.appendSwitch(\"enable-experimental-web-platform-features\");\n\nconst isDummyNew = isDummy;\n\nif (process.env[\"ELECTRON_ENV_PROD\"]) {\n    DevToolsManager.setEnable(false);\n}\n\nconst logDebugContent = (label: string, content: any) => {\n    const filePath = AppEnv.userData + \"/debug.log\";\n    const msg = label + \" - \" + JSON.stringify(content);\n    console.log(msg);\n    fs.appendFileSync(filePath, msg + \"\\n\");\n};\n\nprocess.on(\"uncaughtException\", (reason) => {\n    let error: any = reason;\n    if (error instanceof Error) {\n        error = [error.message, error.stack].join(\"\\n\");\n    }\n    Log.error(\"UncaughtException\", error);\n    reportError(\n        reason instanceof Error ? reason.message : String(reason),\n        reason instanceof Error ? reason.stack : undefined,\n    );\n});\n\nprocess.on(\"unhandledRejection\", (reason) => {\n    let error: any = reason;\n    if (error instanceof Error) {\n        error = [error.message, error.stack].join(\"\\n\");\n    }\n    Log.error(\"UnhandledRejection\", error);\n    reportError(\n        reason instanceof Error ? (reason as Error).message : String(reason),\n        reason instanceof Error ? (reason as Error).stack : undefined,\n    );\n});\n\n// Set application name for Windows 10+ notifications\nif (process.platform === \"win32\") app.setAppUserModelId(app.getName());\n\nif (!app.requestSingleInstanceLock()) {\n    app.quit();\n    process.exit(0);\n}\n\n// app.disableHardwareAcceleration();\n// app.setAccessibilitySupportEnabled(true)\n\nAppEnv.appRoot = process.env.APP_ROOT;\nAppEnv.appData = app.getPath(\"appData\");\nAppEnv.userData = app.getPath(\"userData\");\nAppEnv.dataRoot = path.join(AppEnv.userData, \"data\");\n\nif (!fs.existsSync(AppEnv.dataRoot)) {\n    fs.mkdirSync(AppEnv.dataRoot, { recursive: true });\n}\nfor (const dir of [\"logs\", \"storage\"]) {\n    if (!fs.existsSync(path.join(AppEnv.dataRoot, dir))) {\n        fs.mkdirSync(path.join(AppEnv.dataRoot, dir), { recursive: true });\n    }\n}\n\nAppEnv.isInit = true;\n\nMAPI.init();\nConfigContextMenu.init();\n\nLog.info(\"Starting\");\nLog.info(\"LaunchInfo\", {\n    isPackaged,\n    appRoot: AppEnv.appRoot,\n    appData: AppEnv.appData,\n    userData: AppEnv.userData,\n    dataRoot: AppEnv.dataRoot,\n});\n\nasync function createWindow() {\n    let icon = logoPath;\n    if (process.platform === \"win32\") {\n        icon = icoLogoPath;\n    } else if (process.platform === \"darwin\") {\n        icon = icnsLogoPath;\n    }\n    const { x: wx, y: wy } = AppPosition.get(\n        \"main\",\n        (screenX, screenY, screenWidth, screenHeight) => {\n            // console.log('calculator', {screenX, screenY, screenWidth, screenHeight});\n            return {\n                x: screenX + screenWidth / 2 - WindowConfig.mainWidth / 2,\n                y: screenY + screenHeight / 8,\n            };\n        },\n    );\n    AppRuntime.mainWindow = new BrowserWindow({\n        show: true,\n        title: AppConfig.title,\n        ...(!isPackaged ? { icon } : {}),\n        frame: false,\n        transparent: true,\n        hasShadow: true,\n        // center: true,\n        x: wx,\n        y: wy,\n        useContentSize: true,\n        minWidth: WindowConfig.mainWidth,\n        minHeight: WindowConfig.mainHeight,\n        width: WindowConfig.mainWidth,\n        height: WindowConfig.mainHeight,\n        skipTaskbar: true,\n        resizable: false,\n        maximizable: false,\n        backgroundColor: await AppsMain.defaultDarkModeBackgroundColor(),\n        webPreferences: {\n            preload: preloadDefault,\n            // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n            nodeIntegration: true,\n            webSecurity: false,\n            webviewTag: true,\n            // Consider using contextBridge.exposeInMainWorld\n            // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n            contextIsolation: false,\n            // sandbox: false,\n        },\n    });\n\n    AppRuntime.mainWindow.on(\"closed\", () => {\n        AppRuntime.mainWindow = null;\n    });\n    AppRuntime.mainWindow.on(\"show\", async () => {\n        await executeHooks(AppRuntime.mainWindow, \"Show\");\n    });\n    AppRuntime.mainWindow.on(\"hide\", async () => {\n        await executeHooks(AppRuntime.mainWindow, \"Hide\");\n    });\n\n    rendererLoadPath(AppRuntime.mainWindow, \"index.html\");\n\n    remoteMain.enable(AppRuntime.mainWindow.webContents);\n    AppRuntime.mainWindow.webContents.on(\"did-finish-load\", () => {\n        Page.ready(\"main\");\n        DevToolsManager.autoShow(AppRuntime.mainWindow);\n    });\n    AppRuntime.mainWindow.webContents.setWindowOpenHandler(({ url }) => {\n        if (url.startsWith(\"https://\") || url.startsWith(\"http://\")) {\n            shell.openExternal(url);\n        }\n        return { action: \"deny\" };\n    });\n    DevToolsManager.register(\"Main\", AppRuntime.mainWindow);\n\n    FastPanelMain.init();\n}\n\nconst handleArgsForApp = (argv: string[]) => {\n    let filePath = null;\n    let url = null;\n    for (let i = 1; i < argv.length; i++) {\n        const arg = argv[i];\n        if (arg.startsWith(\"--\")) {\n            continue;\n        }\n        if ([\".\"].includes(arg)) {\n            continue;\n        }\n        if (arg.startsWith(\"focusany://\")) {\n            url = arg;\n            continue;\n        }\n        filePath = arg;\n        break;\n    }\n    if (filePath) {\n        ManagerEditor.openQueue(filePath).then();\n    }\n    if (url) {\n        ProtocolMain.queue(url).then();\n    }\n};\n\napp.on(\"open-file\", (event, path) => {\n    event.preventDefault();\n    ManagerEditor.openQueue(path).then();\n});\n\napp.on(\"open-url\", (event, url) => {\n    event.preventDefault();\n    ProtocolMain.queue(url).then();\n});\n\napp.whenReady()\n    .then(() => {\n        const isRegistered = app.setAsDefaultProtocolClient(\"focusany\");\n        Log.info(\"ProtocolRegistered\", isRegistered);\n        remoteMain.initialize();\n        session.defaultSession.setDisplayMediaRequestHandler(\n            (request, callback) => {\n                desktopCapturer\n                    .getSources({ types: [\"screen\"] })\n                    .then((sources) => {\n                        // Grant access to the first screen found.\n                        callback({ video: sources[0], audio: \"loopback\" });\n                    });\n            },\n        );\n    })\n    .then(ConfigLang.readyAsync)\n    .then(() => {\n        if (isMac) {\n            app.dock.hide();\n        }\n        MAPI.ready();\n        ConfigMenu.ready();\n        ConfigTray.ready();\n        app.on(\"browser-window-created\", (_, window) => {\n            optimizer.watchWindowShortcuts(window);\n        });\n        createWindow().then();\n        handleArgsForApp(process.argv);\n    });\n\napp.on(\"before-quit\", (event) => {\n    if (!(app as any).forceQuit && isPackaged) {\n        event.preventDefault();\n    }\n});\n\napp.on(\"will-quit\", () => {\n    MAPI.destroy();\n});\n\napp.on(\"window-all-closed\", () => {\n    if (process.platform !== \"darwin\") app.quit();\n});\n\napp.on(\"second-instance\", (event, argv) => {\n    if (AppRuntime.mainWindow) {\n        if (AppRuntime.mainWindow.isMinimized()) {\n            AppRuntime.mainWindow.restore();\n        }\n        AppRuntime.mainWindow.show();\n        AppRuntime.mainWindow.focus();\n    }\n    handleArgsForApp(argv);\n});\n\napp.on(\"activate\", () => {\n    const allWindows = BrowserWindow.getAllWindows();\n    if (allWindows.length) {\n        if (!AppRuntime.mainWindow.isVisible()) {\n            AppRuntime.mainWindow.show();\n        }\n        AppRuntime.mainWindow.focus();\n    } else {\n        createWindow().then();\n    }\n});\n"
  },
  {
    "path": "electron/mapi/app/icons.ts",
    "content": "export const icons = {\n    success:\n        '<svg t=\"1733817409678\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1488\" width=\"1024\" height=\"1024\"><path d=\"M512 832c-176.448 0-320-143.552-320-320S335.552 192 512 192s320 143.552 320 320-143.552 320-320 320m0-704C300.256 128 128 300.256 128 512s172.256 384 384 384 384-172.256 384-384S723.744 128 512 128\" fill=\"#FFF\" p-id=\"1489\"></path><path d=\"M619.072 429.088l-151.744 165.888-62.112-69.6a32 32 0 1 0-47.744 42.624l85.696 96a32 32 0 0 0 23.68 10.688h0.192c8.96 0 17.536-3.776 23.616-10.4l175.648-192a32 32 0 0 0-47.232-43.2\" fill=\"#FFF\" p-id=\"1490\"></path></svg>',\n    error: '<svg t=\"1733817396560\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1326\" width=\"1024\" height=\"1024\"><path d=\"M512 128C300.8 128 128 300.8 128 512s172.8 384 384 384 384-172.8 384-384S723.2 128 512 128zM512 832c-179.2 0-320-140.8-320-320s140.8-320 320-320 320 140.8 320 320S691.2 832 512 832z\" fill=\"#FFF\" p-id=\"1327\"></path><path d=\"M672 352c-12.8-12.8-32-12.8-44.8 0L512 467.2 396.8 352C384 339.2 364.8 339.2 352 352S339.2 384 352 396.8L467.2 512 352 627.2c-12.8 12.8-12.8 32 0 44.8s32 12.8 44.8 0L512 556.8l115.2 115.2c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8L556.8 512l115.2-115.2C684.8 384 684.8 364.8 672 352z\" fill=\"#FFF\" p-id=\"1328\"></path></svg>',\n    info: '<svg t=\"1733992721464\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1357\" width=\"1024\" height=\"1024\"><path d=\"M512 898.71874973C299.30468777 898.71874973 125.28125027 724.69531223 125.28125027 512S299.30468777 125.28125027 512 125.28125027s386.71874973 174.0234375 386.71874973 386.71874973-174.0234375 386.71874973-386.71874973 386.71874973z m0-696.09375c-170.15625027 0-309.37500027 139.21875-309.37500027 309.37500027 0 170.15625027 139.21875 309.37500027 309.37500027 309.37500027 170.15625027 0 309.37500027-139.21875 309.37500027-309.37500027 0-170.15625027-139.21875-309.37500027-309.37500027-309.37500027z\" fill=\"#FFFFFF\" p-id=\"1358\"></path><path d=\"M512 746.59765652a37.96875 37.96875 0 0 1-38.67187473-38.67187554v-221.6953125c0-21.9375 16.76953125-38.67187473 38.67187473-38.67187473 21.90234348 0 38.67187473 16.73437473 38.67187473 38.67187473v221.6953125c0 21.9375-16.76953125 38.67187473-38.67187473 38.67187554zM512 390.81640625a37.96875 37.96875 0 0 1-38.67187473-38.67187473V316.07421902c0-21.9375 16.76953125-38.67187473 38.67187473-38.67187554 21.90234348 0 38.67187473 16.73437473 38.67187473 38.67187554v36.0703125c0 21.9375-18.03515625 38.67187473-38.67187473 38.67187473z\" fill=\"#FFFFFF\" p-id=\"1359\"></path></svg>',\n    loading: `\n<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"xMidYMid\">\n    <rect  fill=\"#FFFFFF\" x=\"21.5\" y=\"21.5\" width=\"25\" height=\"25\" rx=\"3\" ry=\"3\">\n      <animate attributeName=\"x\" calcMode=\"linear\" values=\"21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5\" keyTimes=\"0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1\" dur=\"1.5\" begin=\"-1.375s\" repeatCount=\"indefinite\"></animate>\n      <animate attributeName=\"y\" calcMode=\"linear\" values=\"21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5\" keyTimes=\"0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1\" dur=\"1.5\" begin=\"-1s\" repeatCount=\"indefinite\"></animate>\n    </rect>\n    <rect fill=\"#FFFFFF\" x=\"21.5\" y=\"53.5\" width=\"25\" height=\"25\" rx=\"3\" ry=\"3\">\n      <animate attributeName=\"x\" calcMode=\"linear\" values=\"21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5\" keyTimes=\"0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1\" dur=\"1.5\" begin=\"-0.875s\" repeatCount=\"indefinite\"></animate>\n      <animate attributeName=\"y\" calcMode=\"linear\" values=\"21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5\" keyTimes=\"0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1\" dur=\"1.5\" begin=\"-0.5s\" repeatCount=\"indefinite\"></animate>\n    </rect>\n    <rect fill=\"#FFFFFF\" x=\"53.5\" y=\"42.919\" width=\"25\" height=\"25\" rx=\"3\" ry=\"3\">\n      <animate attributeName=\"x\" calcMode=\"linear\" values=\"21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5\" keyTimes=\"0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1\" dur=\"1.5\" begin=\"-0.375s\" repeatCount=\"indefinite\"></animate>\n      <animate attributeName=\"y\" calcMode=\"linear\" values=\"21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5\" keyTimes=\"0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1\" dur=\"1.5\" begin=\"0s\" repeatCount=\"indefinite\"></animate>\n    </rect>\n  </svg>`,\n};\n"
  },
  {
    "path": "electron/mapi/app/index.ts",
    "content": "import iconv from \"iconv-lite\";\nimport { exec as _exec, spawn } from \"node:child_process\";\nimport net from \"node:net\";\nimport util from \"node:util\";\nimport { AppConfig } from \"../../../src/config\";\nimport {\n    extraResolveBin,\n    isLinux,\n    isMac,\n    isWin,\n    platformArch,\n    platformName,\n    platformUUID,\n    platformVersion,\n} from \"../../lib/env\";\nimport { IconvUtil, ShellUtil, StrUtil } from \"../../lib/util\";\nimport { Log } from \"../log/index\";\n\nconst exec = util.promisify(_exec);\n\nconst outputStringConvert = (outputEncoding: \"utf8\" | \"cp936\", data: any) => {\n    if (!data) {\n        return \"\";\n    }\n    if (outputEncoding === \"utf8\") {\n        return data.toString();\n    }\n    let dataEncoding = \"binary\";\n    if (Buffer.isBuffer(data)) {\n        dataEncoding = IconvUtil.detect(data as any);\n        if (\"UTF-8\" === dataEncoding) {\n            return data.toString(\"utf8\");\n        }\n    }\n    // dataEncoding UTF-8 cp936\n    // dataEncoding ISO-8859-1 cp936\n    // console.log('dataEncoding', dataEncoding, outputEncoding)\n    return iconv.decode(Buffer.from(data, dataEncoding as any), outputEncoding);\n};\n\nconst shell = async (\n    command: string,\n    option?: {\n        cwd?: string;\n        outputEncoding?: string;\n        shell?: boolean;\n    },\n) => {\n    option = Object.assign(\n        {\n            cwd: process.cwd(),\n            outputEncoding: isWin ? \"cp936\" : \"utf8\",\n            shell: true,\n        },\n        option,\n    );\n    const result = await exec(command, {\n        env: { ...process.env },\n        shell: option.shell,\n        encoding: \"binary\",\n        cwd: option[\"cwd\"],\n    } as any);\n    return {\n        stdout: outputStringConvert(\n            option.outputEncoding as any,\n            result.stdout,\n        ),\n        stderr: outputStringConvert(\n            option.outputEncoding as any,\n            result.stderr,\n        ),\n    };\n};\n\nconst spawnShell = async (\n    command: string | string[],\n    option: {\n        stdout?: (data: string, process: any) => void;\n        stderr?: (data: string, process: any) => void;\n        success?: (process: any) => void;\n        error?: (msg: string, exitCode: number, process: any) => void;\n        cwd?: string;\n        outputEncoding?: string;\n        env?: Record<string, any>;\n        shell?: boolean;\n    } | null = null,\n): Promise<{\n    stop: () => void;\n    send: (data: any) => void;\n    result: () => Promise<string>;\n}> => {\n    option = Object.assign(\n        {\n            cwd: process.cwd(),\n            outputEncoding: isWin ? \"cp936\" : \"utf8\",\n            env: {},\n            shell: true,\n        },\n        option,\n    );\n    let commandEntry = \"\",\n        args = [];\n    if (Array.isArray(command)) {\n        commandEntry = command[0];\n        args = command.slice(1);\n    } else {\n        args = ShellUtil.parseCommandArgs(command);\n        commandEntry = args.shift() as string;\n    }\n    Log.info(\"App.spawnShell\", {\n        commandEntry,\n        args,\n        option: {\n            cwd: option[\"cwd\"],\n            outputEncoding: option[\"outputEncoding\"],\n        },\n    });\n    const spawnProcess = spawn(commandEntry, args, {\n        env: { ...process.env, ...option.env },\n        cwd: option[\"cwd\"],\n        shell: option.shell,\n        encoding: \"binary\",\n    } as any);\n    let end = false;\n    let isSuccess = false;\n    let exitCode = -1;\n    const stdoutList: string[] = [];\n    const stderrList: string[] = [];\n    spawnProcess.stdout?.on(\"data\", (data) => {\n        // console.log('App.spawnShell.stdout', data)\n        let dataString = outputStringConvert(\n            option.outputEncoding as any,\n            data,\n        );\n        Log.info(\"App.spawnShell.stdout\", dataString);\n        stdoutList.push(dataString);\n        option.stdout?.(dataString, spawnProcess);\n    });\n    spawnProcess.stderr?.on(\"data\", (data) => {\n        // console.log('App.spawnShell.stderr', data)\n        let dataString = outputStringConvert(\n            option.outputEncoding as any,\n            data,\n        );\n        Log.info(\"App.spawnShell.stderr\", dataString);\n        stderrList.push(dataString);\n        option.stderr?.(dataString, spawnProcess);\n    });\n    spawnProcess.on(\"exit\", (code, signal) => {\n        // console.log('App.spawnShell.exit', code)\n        Log.info(\"App.spawnShell.exit\", { code, signal });\n        exitCode = code;\n        if (isWin) {\n            if (0 === code || 1 === code) {\n                isSuccess = true;\n            }\n        } else {\n            if (null === code || 0 === code) {\n                isSuccess = true;\n            }\n        }\n        if (isSuccess) {\n            option.success?.(spawnProcess);\n        } else {\n            option.error?.(\n                `command ${command} failed with code ${code}`,\n                exitCode,\n                spawnProcess,\n            );\n        }\n        end = true;\n    });\n    spawnProcess.on(\"error\", (err) => {\n        // console.log('App.spawnShell.error', err)\n        Log.info(\"App.spawnShell.error\", err);\n        option.error?.(err.toString(), -1, spawnProcess);\n        end = true;\n    });\n    return {\n        stop: () => {\n            Log.info(\"App.spawnShell.stop\");\n            if (isWin) {\n                _exec(\n                    `taskkill /pid ${spawnProcess.pid} /T /F`,\n                    {\n                        encoding: \"binary\",\n                    },\n                    (err, stdout, stderr) => {\n                        if (stdout) {\n                            stdout = outputStringConvert(\n                                option.outputEncoding as any,\n                                stdout,\n                            );\n                        }\n                        if (stderr) {\n                            stderr = outputStringConvert(\n                                option.outputEncoding as any,\n                                stderr,\n                            );\n                        }\n                        Log.info(\n                            \"App.spawnShell.stop.taskkill\",\n                            JSON.parse(JSON.stringify({ err, stdout, stderr })),\n                        );\n                    },\n                );\n            } else {\n                spawnProcess.kill(\"SIGINT\");\n            }\n        },\n        send: (data) => {\n            Log.info(\"App.spawnShell.send\", data);\n            spawnProcess.stdin.write(data);\n        },\n        result: async (): Promise<string> => {\n            if (end) {\n                return stdoutList.join(\"\") + stderrList.join(\"\");\n            }\n            return new Promise((resolve, reject) => {\n                const watchEnd = () => {\n                    setTimeout(() => {\n                        if (!end) {\n                            watchEnd();\n                            return;\n                        }\n                        if (isSuccess) {\n                            resolve(stdoutList.join(\"\") + stderrList.join(\"\"));\n                        } else {\n                            reject(\n                                [\n                                    `command ${command} failed with code ${exitCode} : `,\n                                    stdoutList.join(\"\"),\n                                    stderrList.join(\"\"),\n                                ].join(\"\"),\n                            );\n                        }\n                    }, 10);\n                };\n                watchEnd();\n            });\n        },\n    };\n};\n\nconst spawnBinary = async (\n    binary: string,\n    args: string[],\n    option: {\n        stdout?: (data: string, process: any) => void;\n        stderr?: (data: string, process: any) => void;\n        success?: (process: any) => void;\n        error?: (msg: string, exitCode: number, process: any) => void;\n        cwd?: string;\n        outputEncoding?: string;\n        env?: Record<string, any>;\n        shell?: boolean;\n    } | null = null,\n): Promise<string> => {\n    args.unshift(extraResolveBin(binary));\n    const res = await Apps.spawnShell(args, {\n        ...(option || {}),\n        shell: false,\n    });\n    return await res.result();\n};\n\nconst availablePortLock: {\n    [port: number]: {\n        lockKey: string;\n        lockTime: number;\n    };\n} = {};\n\n/**\n * 获取一个可用的端口\n * @param start 开始的端口\n * @param lockKey 锁定的key，避免其他进程获取，默认会创建一个随机的key\n * @param lockTime 锁定时间，避免在本次获取后未启动服务导致其他进程重复获取\n */\nconst availablePort = async (\n    start: number,\n    lockKey?: string,\n    lockTime?: number,\n): Promise<number> => {\n    lockKey = lockKey || StrUtil.randomString(8);\n    lockTime = lockTime || 60;\n    // expire lock\n    const now = Date.now();\n    for (const port in availablePortLock) {\n        const lockInfo = availablePortLock[port];\n        if (lockInfo.lockTime < now) {\n            delete availablePortLock[port];\n        }\n    }\n    for (let i = start; i < 65535; i++) {\n        const available = await isPortAvailable(i, \"0.0.0.0\");\n        const availableLocal = await isPortAvailable(i, \"127.0.0.1\");\n        // console.log('isPortAvailable', i, available, availableLocal)\n        if (available && availableLocal) {\n            const lockInfo = availablePortLock[i];\n            if (lockInfo) {\n                if (lockInfo.lockKey === lockKey) {\n                    return i;\n                } else {\n                    // other lockKey lock the port\n                    continue;\n                }\n            }\n            availablePortLock[i] = {\n                lockKey,\n                lockTime: Date.now() + lockTime * 1000,\n            };\n            return i;\n        }\n    }\n    throw new Error(\"no available port\");\n};\n\nconst isPortAvailable = async (\n    port: number,\n    host?: string,\n): Promise<boolean> => {\n    return new Promise((resolve) => {\n        const server = net.createServer();\n        server.listen(port, host);\n        server.on(\"listening\", () => {\n            server.close();\n            resolve(true);\n        });\n        server.on(\"error\", () => {\n            resolve(false);\n        });\n    });\n};\n\nconst fixExecutable = async (executable: string) => {\n    if (isMac || isLinux) {\n        // chmod +x executable\n        await shell(`chmod +x \"${executable}\"`);\n    }\n};\n\nconst getUserAgent = () => {\n    let param = [];\n    param.push(`AppOpen/${AppConfig.name}/${AppConfig.version}`);\n    param.push(\n        `Platform/${platformName()}/${platformArch()}/${platformVersion()}/${platformUUID()}`,\n    );\n    return param.join(\" \");\n};\n\nexport const Apps = {\n    shell,\n    spawnShell,\n    spawnBinary,\n    availablePort,\n    isPortAvailable,\n    fixExecutable,\n    getUserAgent,\n};\n\nexport default Apps;\n"
  },
  {
    "path": "electron/mapi/app/lib/position.ts",
    "content": "import { screen } from \"electron\";\n\ntype PositionCache = {\n    x: 0;\n    y: 0;\n    screenWidth: 0;\n    screenHeight: 0;\n    id: -1;\n};\n\nexport const AppPosition = {\n    caches: {} as Record<string, PositionCache>,\n    getCache(name: string): PositionCache {\n        if (!this.caches[name]) {\n            this.caches[name] = {\n                x: 0,\n                y: 0,\n                screenWidth: 0,\n                screenHeight: 0,\n                id: -1,\n            };\n        }\n        return this.caches[name];\n    },\n    get(\n        name: string,\n        calculator?: (\n            screenX: number,\n            screenY: number,\n            screenWidth: number,\n            screenHeight: number,\n        ) => {\n            x: number;\n            y: number;\n        },\n    ): {\n        x: number;\n        y: number;\n    } {\n        const cache = this.getCache(name);\n        const { x, y } = screen.getCursorScreenPoint();\n        const currentDisplay = screen.getDisplayNearestPoint({ x, y });\n        if (cache.id !== currentDisplay.id) {\n            cache.id = currentDisplay.id;\n            cache.screenWidth = currentDisplay.workArea.width;\n            cache.screenHeight = currentDisplay.workArea.height;\n            if (!calculator) {\n                calculator = (\n                    screenX: number,\n                    screenY: number,\n                    screenWidth: number,\n                    screenHeight: number,\n                ) => {\n                    // console.log('calculator', {screenX, screenY, screenWidth, screenHeight});\n                    return {\n                        x: screenX + screenWidth / 10,\n                        y: screenY + screenHeight / 10,\n                    };\n                };\n            }\n            const res = calculator(\n                currentDisplay.workArea.x,\n                currentDisplay.workArea.y,\n                cache.screenWidth,\n                cache.screenHeight,\n            );\n            cache.x = parseInt(String(res.x));\n            cache.y = parseInt(String(res.y));\n        }\n        return {\n            x: cache.x,\n            y: cache.y,\n        };\n    },\n    set(name: string, x: number, y: number): void {\n        const cache = this.getCache(name);\n        cache.x = x;\n        cache.y = y;\n    },\n    getContextMenuPosition(\n        boxWidth: number,\n        boxHeight: number,\n    ): {\n        x: number;\n        y: number;\n    } {\n        const { x, y } = screen.getCursorScreenPoint();\n        const currentDisplay = screen.getDisplayNearestPoint({ x, y });\n        let resultX = x;\n        let resultY = y;\n        if (currentDisplay.workArea.width - x < boxWidth) {\n            resultX = currentDisplay.workArea.width - boxWidth;\n        }\n        if (currentDisplay.workArea.height - y < boxHeight) {\n            resultY = currentDisplay.workArea.height - boxHeight;\n        }\n        return {\n            x: resultX,\n            y: resultY,\n        };\n    },\n};\n"
  },
  {
    "path": "electron/mapi/app/loading.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { AppsMain } from \"./main\";\nimport { icons } from \"./icons\";\n\nexport const makeLoading = (\n    msg: string,\n    options?: {\n        timeout?: number;\n        percentAuto?: boolean;\n        percentTotalSeconds?: number;\n    },\n): {\n    close: () => void;\n    percent: (value: number) => void;\n} => {\n    options = Object.assign(\n        {\n            percentAuto: false,\n            percentTotalSeconds: 30,\n            timeout: 0,\n        },\n        options,\n    );\n\n    if (options.timeout === 0) {\n        options.timeout = 60 * 10 * 1000;\n    }\n    // console.log('options', options)\n\n    const display = AppsMain.getCurrentScreenDisplay();\n    // console.log('xxxx', primaryDisplay);\n    const width = display.workArea.width;\n    const height = 60;\n    const icon = icons.loading;\n\n    const win = new BrowserWindow({\n        height,\n        width,\n        x: 0,\n        y: 0,\n        modal: false,\n        frame: false,\n        alwaysOnTop: true,\n        center: false,\n        transparent: true,\n        hasShadow: false,\n        show: false,\n        focusable: false,\n        skipTaskbar: true,\n    });\n    const htmlContent = `\n  <!DOCTYPE html>\n  <html>\n    <head>\n      <style>\n        html,body{\n            height: 100vh;\n            margin: 0;\n            padding: 0;\n            background: rgba(0, 0, 0, 0.4);\n            color: #FFFFFF;\n        }\n        .message-view {\n            height: 100vh;\n            display:flex;\n            text-align:center;\n            padding:0 10px;\n            position:relative;\n        }\n        .message-view #message{\n            margin: auto;\n            font-size: 16px;\n            display: inline-block;\n            line-height: 30px;\n            white-space: nowrap;\n        }\n        .message-view #message .icon{\n            width: 30px;\n            height: 30px;\n            display:inline-block;\n            margin-right: 5px;\n            vertical-align: top;\n        }\n        .message-view #percent{\n            position: absolute;\n            bottom: 5px;\n            left: 5px;\n            right: 5px;\n            height: 5px;\n            border-radius: 5px;\n            background: rgba(255, 255, 255, 0.4);\n            overflow: hidden;\n            display:none;\n        }\n        .message-view #percent .value{\n            border-radius: 5px;\n            height: 100%;\n            width: 0%;\n            background: #FFFFFF;\n        }\n        ::-webkit-scrollbar {\n          width: 0;\n        }\n      </style>\n    </head>\n    <body>\n      <div class=\"message-view\">\n        <div id=\"message\">${icon}${msg}</div>\n        <div id=\"percent\">\n            <div class=\"value\"></div>\n        </div>\n      </div>\n    </body>\n  </html>\n`;\n\n    const encodedHTML = encodeURIComponent(htmlContent);\n    let percentAutoTimer = null;\n    win.loadURL(`data:text/html;charset=UTF-8,${encodedHTML}`);\n    win.on(\"ready-to-show\", async () => {\n        const width = Math.ceil(\n            await win.webContents.executeJavaScript(`(()=>{\n            const message = document.getElementById('message');\n            const width = message.scrollWidth;\n            return width;\n        })()`),\n        );\n        win.setSize(width + 20, height);\n        const x =\n            display.workArea.x + display.workArea.width / 2 - (width + 20) / 2;\n        const y = display.workArea.y + (display.workArea.height * 1) / 4;\n        win.setPosition(Math.floor(x), Math.floor(y));\n        win.show();\n        if (options.percentAuto) {\n            let percent = 0;\n            percentAutoTimer = setInterval(\n                () => {\n                    percent += 0.01;\n                    if (percent >= 1) {\n                        clearInterval(percentAutoTimer);\n                        return;\n                    }\n                    controller.percent(percent);\n                },\n                (options.percentTotalSeconds * 1000) / 100,\n            );\n        }\n        // win.webContents.openDevTools({\n        //     mode: 'detach'\n        // })\n    });\n    const winCloseTimer = setTimeout(() => {\n        win.close();\n        clearTimeout(winCloseTimer);\n    }, options.timeout);\n    const controller = {\n        close: () => {\n            win.close();\n            clearTimeout(winCloseTimer);\n            if (percentAutoTimer) {\n                clearInterval(percentAutoTimer);\n            }\n        },\n        percent: (value: number) => {\n            const percent = 100 * value;\n            win.webContents.executeJavaScript(`(()=>{\n                const percent = document.querySelector('#percent');\n                const percentValue = document.querySelector('#percent .value');\n                percent.style.display = 'block';\n                percentValue.style.width = '${percent}%';\n            })()`);\n        },\n    };\n    return controller;\n};\n"
  },
  {
    "path": "electron/mapi/app/main.ts",
    "content": "import {\n    app,\n    BrowserWindow,\n    clipboard,\n    ipcMain,\n    nativeImage,\n    nativeTheme,\n    screen,\n    shell,\n} from \"electron\";\nimport { AppConfig } from \"../../../src/config\";\nimport { CommonConfig } from \"../../config/common\";\nimport { WindowConfig } from \"../../config/window\";\nimport {\n    isDev,\n    isMac,\n    platformArch,\n    platformName,\n    platformUUID,\n    platformVersion,\n} from \"../../lib/env\";\nimport { preloadDefault, rendererDistPath } from \"../../lib/env-main\";\nimport { Page } from \"../../page\";\nimport { ConfigMain } from \"../config/main\";\nimport { AppRuntime } from \"../env\";\nimport { Events } from \"../event/main\";\nimport { Files } from \"../file/main\";\nimport Apps from \"./index\";\nimport { AppPosition } from \"./lib/position\";\nimport { makeLoading } from \"./loading\";\nimport { SetupMain } from \"./setup\";\nimport { makeToast } from \"./toast\";\n\nconst getWindowByName = (name?: string) => {\n    if (!name || \"main\" === name) {\n        return AppRuntime.mainWindow;\n    }\n    if (\"fastPanel\" === name) {\n        return AppRuntime.fastPanelWindow;\n    }\n    return AppRuntime.windows[name];\n};\n\nconst getCurrentWindow = (window, e) => {\n    let originWindow = BrowserWindow.fromWebContents(e.sender);\n    // if (originWindow !== window) originWindow = detachInstance.getWindow();\n    return originWindow;\n};\n\nconst quit = () => {\n    app.quit();\n};\n\nipcMain.handle(\"app:quit\", () => {\n    quit();\n});\n\nconst restart = () => {\n    app.relaunch();\n};\n\nipcMain.handle(\"app:restart\", () => {\n    restart();\n});\n\nconst windowMin = (name?: string) => {\n    getWindowByName(name)?.minimize();\n};\n\nconst windowMax = (name?: string) => {\n    const win = getWindowByName(name);\n    if (!win) {\n        return;\n    }\n    if (win.isFullScreen()) {\n        win.setFullScreen(false);\n        win.unmaximize();\n        win.center();\n    } else if (win.isMaximized()) {\n        win.unmaximize();\n        win.center();\n    } else {\n        win.setMinimumSize(WindowConfig.minWidth, WindowConfig.minHeight);\n        win.maximize();\n    }\n};\n\nconst windowSetSize = (\n    name: string | null,\n    width: number,\n    height: number,\n    option?: {\n        includeMinimumSize: boolean;\n        center: boolean;\n    },\n) => {\n    width = parseInt(String(width));\n    height = parseInt(String(height));\n    // console.log('windowSetSize', name, width, height, option)\n    const win = getWindowByName(name);\n    if (!win) {\n        return;\n    }\n    option = Object.assign(\n        {\n            includeMinimumSize: true,\n            center: true,\n        },\n        option,\n    );\n    if (option.includeMinimumSize) {\n        win.setMinimumSize(width, height);\n    }\n    win.setSize(width, height);\n    if (option.center) {\n        win.center();\n    }\n};\n\nipcMain.handle(\"app:openExternal\", (event, url: string) => {\n    return shell.openExternal(url);\n});\nipcMain.handle(\"app:openPath\", (event, url: string) => {\n    return shell.openPath(url);\n});\nipcMain.handle(\"app:showItemInFolder\", (event, url: string) => {\n    return shell.showItemInFolder(url);\n});\n\nipcMain.handle(\"app:getPreload\", (event) => {\n    let preload = preloadDefault;\n    if (!preload.startsWith(\"file://\")) {\n        preload = `file://${preload}`;\n    }\n    return preload;\n});\n\nipcMain.handle(\"window:min\", (event, name: string) => {\n    windowMin(name);\n});\nipcMain.handle(\"window:max\", (event, name: string) => {\n    windowMax(name);\n});\nipcMain.handle(\n    \"window:setSize\",\n    (\n        event,\n        name: string | null,\n        width: number,\n        height: number,\n        option?: {\n            includeMinimumSize: boolean;\n            center: boolean;\n        },\n    ) => {\n        windowSetSize(name, width, height, option);\n    },\n);\n\nipcMain.handle(\"window:close\", (event, name: string) => {\n    getWindowByName(name)?.close();\n});\n\nconst windowOpen = async (\n    name: string,\n    option?: {\n        singleton?: boolean;\n        parent?: BrowserWindow;\n        [key: string]: any;\n    },\n) => {\n    name = name || \"main\";\n    return Page.open(name, option);\n};\n\nipcMain.handle(\"window:open\", (event, name: string, option: any) => {\n    return windowOpen(name, option);\n});\n\nipcMain.handle(\"window:hide\", (event, name: string) => {\n    getWindowByName(name)?.hide();\n    if (isMac) {\n        app.dock.hide();\n    }\n});\n\nipcMain.handle(\n    \"window:move\",\n    (\n        event,\n        name: string | null,\n        data: {\n            mouseX: number;\n            mouseY: number;\n            width: number;\n            height: number;\n        },\n    ) => {\n        const { x, y } = screen.getCursorScreenPoint();\n        const originWindow = getWindowByName(name);\n        if (!originWindow) return;\n        originWindow.setBounds({\n            x: x - data.mouseX,\n            y: y - data.mouseY,\n            width: data.width,\n            height: data.height,\n        });\n        AppPosition.set(name, x - data.mouseX, y - data.mouseY);\n    },\n);\n\nconst getClipboardText = () => {\n    return clipboard.readText(\"clipboard\");\n};\n\nipcMain.handle(\"app:getClipboardText\", (event) => {\n    return getClipboardText();\n});\n\nconst setClipboardText = (text: string) => {\n    clipboard.writeText(text, \"clipboard\");\n};\n\nipcMain.handle(\"app:setClipboardText\", (event, text: string) => {\n    setClipboardText(text);\n});\n\nconst getClipboardImage = () => {\n    const image = clipboard.readImage(\"clipboard\");\n    return image.isEmpty() ? \"\" : image.toDataURL();\n};\n\nipcMain.handle(\"app:getClipboardImage\", (event) => {\n    return getClipboardImage();\n});\n\nconst setClipboardImage = (image: string) => {\n    const img = nativeImage.createFromDataURL(image);\n    clipboard.writeImage(img, \"clipboard\");\n};\n\nipcMain.handle(\"app:setClipboardImage\", (event, image: string) => {\n    setClipboardImage(image);\n});\n\nconst isDarkMode = () => {\n    if (!CommonConfig.darkModeEnable) {\n        return false;\n    }\n    return nativeTheme.shouldUseDarkColors;\n};\n\nconst shouldDarkMode = async () => {\n    if (!CommonConfig.darkModeEnable) {\n        return false;\n    }\n    const darkMode = (await ConfigMain.get(\"darkMode\")) || \"auto\";\n    if (\"dark\" === darkMode) {\n        return true;\n    } else if (\"light\" === darkMode) {\n        return false;\n    } else if (\"auto\" === darkMode) {\n        return isDarkMode();\n    }\n    return false;\n};\n\nconst defaultDarkModeBackgroundColor = async () => {\n    if (await shouldDarkMode()) {\n        return \"#17171A\";\n    }\n    return \"#00FFFFFF\";\n};\n\nnativeTheme.on(\"updated\", () => {\n    Events.broadcast(\"DarkModeChange\", { isDarkMode: isDarkMode() });\n    AppsMain.defaultDarkModeBackgroundColor().then((color) => {\n        AppRuntime.mainWindow.setBackgroundColor(color);\n    });\n});\n\nipcMain.handle(\"app:isDarkMode\", () => {\n    return isDarkMode();\n});\n\nconst getCurrentScreenDisplay = () => {\n    const screenPoint = screen.getCursorScreenPoint();\n    const display = screen.getDisplayNearestPoint(screenPoint);\n    return {\n        bounds: display.bounds,\n        workArea: display.workArea,\n    };\n};\n\nconst calcPositionInCurrentDisplay = (\n    position:\n        | \"center\"\n        | \"left-top\"\n        | \"right-top\"\n        | \"left-bottom\"\n        | \"right-bottom\",\n    width: number,\n    height: number,\n) => {\n    const { bounds, workArea } = getCurrentScreenDisplay();\n    let x = 0;\n    let y = 0;\n    switch (position) {\n        case \"center\":\n            x = workArea.x + (workArea.width - width) / 2;\n            y = workArea.y + (workArea.height - height) / 2;\n            break;\n        case \"left-top\":\n            x = workArea.x;\n            y = workArea.y;\n            break;\n        case \"right-top\":\n            x = workArea.x + workArea.width - width;\n            y = workArea.y;\n            break;\n        case \"left-bottom\":\n            x = workArea.x;\n            y = workArea.y + workArea.height - height;\n            break;\n        case \"right-bottom\":\n            x = workArea.x + workArea.width - width;\n            y = workArea.y + workArea.height - height;\n            break;\n    }\n    return {\n        x: Math.round(x),\n        y: Math.round(y),\n    };\n};\n\nconst toast = (\n    msg: string,\n    options?: {\n        duration?: number;\n        status?: \"success\" | \"error\" | \"info\";\n    },\n) => {\n    return makeToast(msg, options);\n};\n\nipcMain.handle(\"app:toast\", (event, msg: string, option?: any) => {\n    return toast(msg, option);\n});\n\nconst loading = (\n    msg: string,\n    options?: {\n        timeout?: number;\n        percentAuto?: boolean;\n        percentTotalSeconds?: number;\n    },\n): {\n    close: () => void;\n    percent: (value: number) => void;\n} => {\n    return makeLoading(msg, options);\n};\n\nipcMain.handle(\"app:loading\", (event, msg: string, option?: any) => {\n    return loading(msg, option);\n});\n\nipcMain.handle(\"app:setupList\", async () => {\n    return SetupMain.list();\n});\n\nipcMain.handle(\"app:setupOpen\", async (event, name: string) => {\n    return SetupMain.open(name);\n});\n\nconst setupIsOk = async () => {\n    return SetupMain.isOk();\n};\n\nipcMain.handle(\"app:setupIsOk\", async () => {\n    return setupIsOk();\n});\n\nconst getBuildInfo = async () => {\n    if (isDev) {\n        return {\n            buildId: \"Development\",\n        };\n    }\n    const json = await Files.read(rendererDistPath(\"build.json\"), {\n        isDataPath: false,\n    });\n    return JSON.parse(json);\n};\n\nipcMain.handle(\"app:getBuildInfo\", async () => {\n    return getBuildInfo();\n});\n\nconst collect = async (options?: {}) => {\n    return {\n        userAgent: Apps.getUserAgent(),\n        name: AppConfig.name,\n        version: AppConfig.version,\n        uuid: platformUUID(),\n        platformVersion: platformVersion(),\n        platformName: platformName(),\n        platformArch: platformArch(),\n    };\n};\n\nipcMain.handle(\"app:collect\", async (event, options?: {}) => {\n    return collect(options);\n});\n\nconst setAutoLaunch = async (enable: boolean, options?: {}) => {\n    return app.setLoginItemSettings({\n        openAtLogin: enable,\n    });\n};\n\nipcMain.handle(\n    \"app:setAutoLaunch\",\n    async (event, enable: boolean, options?: {}) => {\n        return setAutoLaunch(enable, options);\n    },\n);\n\nconst getAutoLaunch = async (options?: {}) => {\n    return app.getLoginItemSettings().openAtLogin;\n};\n\nipcMain.handle(\"app:getAutoLaunch\", async (event, options?: {}) => {\n    return getAutoLaunch(options);\n});\n\nexport default {\n    quit,\n};\n\nexport const AppsMain = {\n    shouldDarkMode,\n    defaultDarkModeBackgroundColor,\n    getWindowByName,\n    getClipboardText,\n    setClipboardText,\n    getClipboardImage,\n    setClipboardImage,\n    getCurrentScreenDisplay,\n    calcPositionInCurrentDisplay,\n    toast,\n    loading,\n    setupIsOk,\n    windowOpen,\n};\n"
  },
  {
    "path": "electron/mapi/app/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\nimport { resolve } from \"node:path\";\nimport { isPackaged, platformArch, platformName } from \"../../lib/env\";\nimport { AppEnv, waitAppEnvReady } from \"../env\";\nimport appIndex from \"./index\";\n\nconst isDarkMode = async () => {\n    return ipcRenderer.invoke(\"app:isDarkMode\");\n};\n\nconst quit = () => {\n    return ipcRenderer.invoke(\"app:quit\");\n};\n\nconst restart = () => {\n    return ipcRenderer.invoke(\"app:restart\");\n};\n\nconst isPlatform = (name: \"win\" | \"osx\" | \"linux\") => {\n    return platformName() === name;\n};\n\nconst windowMin = (name?: string) => {\n    return ipcRenderer.invoke(\"window:min\", name);\n};\n\nconst windowMax = (name?: string) => {\n    return ipcRenderer.invoke(\"window:max\", name);\n};\n\nconst windowSetSize = (\n    name: string | null,\n    width: number,\n    height: number,\n    option?: {\n        includeMinimumSize: boolean;\n        center: boolean;\n    },\n) => {\n    return ipcRenderer.invoke(\"window:setSize\", name, width, height, option);\n};\n\nconst windowOpen = (name: string, option: any) => {\n    return ipcRenderer.invoke(\"window:open\", name, option);\n};\n\nconst windowHide = (name: string) => {\n    return ipcRenderer.invoke(\"window:hide\", name);\n};\n\nconst windowClose = (name: string) => {\n    return ipcRenderer.invoke(\"window:close\", name);\n};\n\nconst windowMove = (\n    name: string | null,\n    data: { mouseX: number; mouseY: number; width: number; height: number },\n) => {\n    return ipcRenderer.invoke(\"window:move\", name, data);\n};\n\nconst openExternal = (url: string) => {\n    return ipcRenderer.invoke(\"app:openExternal\", url);\n};\n\nconst openPath = (url: string) => {\n    return ipcRenderer.invoke(\"app:openPath\", url);\n};\n\nconst showItemInFolder = (url: string) => {\n    return ipcRenderer.invoke(\"app:showItemInFolder\", url);\n};\n\nconst getPreload = async () => {\n    return ipcRenderer.invoke(\"app:getPreload\");\n};\n\nconst resourcePathResolve = async (filePath: string) => {\n    await waitAppEnvReady();\n    const basePath = isPackaged ? process.resourcesPath : AppEnv.appRoot;\n    return resolve(basePath, filePath);\n};\n\nconst extraPathResolve = async (filePath: string) => {\n    await waitAppEnvReady();\n    const basePath = isPackaged ? process.resourcesPath : \"electron/resources\";\n    return resolve(basePath, \"extra\", filePath);\n};\n\nconst appEnv = async () => {\n    await waitAppEnvReady();\n    return AppEnv;\n};\n\nconst setRenderAppEnv = (env: any) => {\n    AppEnv.isInit = true;\n    AppEnv.appRoot = env.appRoot;\n    AppEnv.appData = env.appData;\n    AppEnv.userData = env.userData;\n    AppEnv.dataRoot = env.dataRoot;\n};\n\nconst getClipboardText = () => {\n    return ipcRenderer.invoke(\"app:getClipboardText\");\n};\n\nconst setClipboardText = (text: string) => {\n    return ipcRenderer.invoke(\"app:setClipboardText\", text);\n};\n\nconst getClipboardImage = () => {\n    return ipcRenderer.invoke(\"app:getClipboardImage\");\n};\n\nconst setClipboardImage = (image: string) => {\n    return ipcRenderer.invoke(\"app:setClipboardImage\", image);\n};\n\nconst toast = (msg: string, option?: any) => {\n    return ipcRenderer.invoke(\"app:toast\", msg, option);\n};\n\nconst setupList = () => {\n    return ipcRenderer.invoke(\"app:setupList\");\n};\n\nconst setupOpen = (name: string) => {\n    return ipcRenderer.invoke(\"app:setupOpen\", name);\n};\n\nconst setupIsOk = async () => {\n    return ipcRenderer.invoke(\"app:setupIsOk\");\n};\n\nconst getBuildInfo = async () => {\n    return ipcRenderer.invoke(\"app:getBuildInfo\");\n};\n\nconst collect = async (options?: {}) => {\n    return ipcRenderer.invoke(\"app:collect\", options);\n};\n\nconst setAutoLaunch = async (enable: boolean, options?: {}) => {\n    return ipcRenderer.invoke(\"app:setAutoLaunch\", enable, options);\n};\n\nconst getAutoLaunch = async (options?: {}) => {\n    return ipcRenderer.invoke(\"app:getAutoLaunch\", options);\n};\n\nexport const AppsRender = {\n    isDarkMode,\n    resourcePathResolve,\n    extraPathResolve,\n    platformName,\n    platformArch,\n    isPlatform,\n    quit,\n    restart,\n    windowMin,\n    windowMax,\n    windowSetSize,\n    windowOpen,\n    windowHide,\n    windowClose,\n    windowMove,\n    openExternal,\n    openPath,\n    showItemInFolder,\n    getPreload,\n    appEnv,\n    setRenderAppEnv,\n    getClipboardText,\n    setClipboardText,\n    getClipboardImage,\n    setClipboardImage,\n    toast,\n    setupList,\n    setupOpen,\n    setupIsOk,\n    getBuildInfo,\n    collect,\n    setAutoLaunch,\n    getAutoLaunch,\n    shell: appIndex.shell,\n    spawnShell: appIndex.spawnShell,\n    spawnBinary: appIndex.spawnBinary,\n    availablePort: appIndex.availablePort,\n    fixExecutable: appIndex.fixExecutable,\n    getUserAgent: appIndex.getUserAgent,\n};\n\nexport default AppsRender;\n"
  },
  {
    "path": "electron/mapi/app/setup.ts",
    "content": "import { Permissions } from \"../../lib/permission\";\nimport { rendererDistPath } from \"../../lib/env-main\";\n\nexport const SetupMain = {\n    async isOk() {\n        if (!(await Permissions.checkAccessibilityAccess())) {\n            return false;\n        }\n        if (!(await Permissions.checkScreenCaptureAccess())) {\n            return false;\n        }\n        return true;\n    },\n    async list() {\n        return [\n            {\n                name: \"accessibility\",\n                title: t(\"setup.accessibility.title\"),\n                status: (await Permissions.checkAccessibilityAccess())\n                    ? \"success\"\n                    : \"fail\",\n                desc: t(\"setup.accessibility.desc\"),\n                steps: [\n                    {\n                        title: t(\"setup.accessibility.step\"),\n                        image: rendererDistPath(\"setup/accessibility.png\"),\n                    },\n                ],\n            },\n            {\n                name: \"screen\",\n                title: t(\"setup.screen.title\"),\n                status: (await Permissions.checkScreenCaptureAccess())\n                    ? \"success\"\n                    : \"fail\",\n                desc: t(\"setup.screen.desc\"),\n                steps: [\n                    {\n                        title: t(\"setup.screen.step\"),\n                        image: rendererDistPath(\"setup/screen.png\"),\n                    },\n                ],\n            },\n        ];\n    },\n    async open(name: string) {\n        switch (name) {\n            case \"accessibility\":\n                Permissions.askAccessibilityAccess().then();\n                break;\n            case \"screen\":\n                Permissions.askScreenCaptureAccess().then();\n                break;\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/app/toast.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { icons } from \"./icons\";\nimport { AppsMain } from \"./main\";\n\nlet win = null;\nlet winCloseTimer = null;\nlet winShowTime = null;\nconst toastMsgQueue: { msg: string; options: any }[] = [];\n\nexport const makeToast = async (\n    msg: string,\n    options?: {\n        duration?: number;\n        status?: \"success\" | \"error\" | \"info\";\n    },\n) => {\n    if (win) {\n        if (winShowTime && Date.now() - winShowTime < 1000) {\n            // make previous toast last at least 1 second\n            if (toastMsgQueue.length > 0) {\n                toastMsgQueue.forEach((item) => {\n                    item.options = Object.assign({}, item.options, {\n                        duration: 1000,\n                    });\n                });\n            }\n            toastMsgQueue.push({ msg, options });\n            await new Promise((resolve) =>\n                setTimeout(resolve, 1000 - (Date.now() - winShowTime)),\n            );\n            if (win) {\n                win.close();\n            }\n            return;\n        }\n        win.close();\n    }\n    winShowTime = Date.now();\n\n    options = Object.assign(\n        {\n            status: \"info\",\n            duration: 0,\n        },\n        options,\n    );\n\n    if (options.duration === 0) {\n        options.duration = Math.max(msg.length * 400, 3000);\n    }\n    // console.log('toast', msg, options)\n\n    const display = AppsMain.getCurrentScreenDisplay();\n    // console.log('xxxx', primaryDisplay);\n    const width = display.workArea.width;\n    const height = 60;\n    const icon = icons[options.status] || icons.success;\n\n    win = new BrowserWindow({\n        height,\n        width,\n        parent: null,\n        x: 0,\n        y: 0,\n        modal: false,\n        frame: false,\n        alwaysOnTop: true,\n        // opacity: 0.9,\n        center: false,\n        transparent: true,\n        hasShadow: false,\n        show: false,\n        focusable: false,\n        skipTaskbar: true,\n    });\n    const htmlContent = `\n    <!DOCTYPE html>\n    <html>\n        <head>\n            <style>\n                html,body{\n                        height: 100%;\n                        margin: 0;\n                        padding: 0;\n                        background: transparent;\n                        color: #FFFFFF;\n                        font-family: \"PingFang SC\", \"Helvetica Neue\", Helvetica, STHeiTi, \"Microsoft YaHei\", \"WenQuanYi Micro Hei\", sans-serif;\n                }\n                .message-view {\n                        height: 100%;\n                        text-align:center;\n                        box-sizing: border-box;\n                        background-color:transparent;\n                        padding: 10px;\n                }\n                .message-view div{\n                        margin: auto;\n                        font-size: 16px;\n                        display: inline-flex;\n                        align-items: center;\n                        line-height: 20px;\n                        background: rgba(0, 0, 0, 0.85);\n                        border-radius: 15px;\n                        padding: 10px 10px;\n                        box-shadow: 5px 5px 5px rgba(0,0,0,0.3);\n                }\n                .message-view div .icon{\n                        width: 30px;\n                        height: 30px;\n                        display:inline-block;\n                        margin-right: 8px;\n                        vertical-align: middle;\n                        flex-shrink: 0;\n                }\n                ::-webkit-scrollbar {\n                    width: 0;\n                }\n            </style>\n        </head>\n        <body>\n            <div class=\"message-view\" onclick=\"window.close()\">\n                <div id=\"message\">${icon}${msg}</div>\n            </div>\n        </body>\n    </html>\n`;\n\n    const encodedHTML = encodeURIComponent(htmlContent);\n    win.loadURL(`data:text/html;charset=UTF-8,${encodedHTML}`);\n    win.on(\"ready-to-show\", async () => {\n        if (!win) return;\n        const containerSize = await win.webContents.executeJavaScript(`(()=>{\n            const message = document.getElementById('message');\n            const width = message.scrollWidth;\n            const height = message.scrollHeight;\n            return {width:width,height:height};\n        })()`);\n        // console.log('containerSize', containerSize);\n        const containerWidth = containerSize.width + 20;\n        const containerHeight = containerSize.height + 20;\n        win.setSize(containerWidth, containerHeight);\n        const x =\n            display.workArea.x +\n            display.workArea.width / 2 -\n            containerWidth / 2;\n        const y = display.workArea.y + (display.workArea.height * 1) / 4;\n        win.setPosition(Math.floor(x), Math.floor(y));\n        win.showInactive();\n        // win.webContents.openDevTools({\n        //     mode: 'detach'\n        // })\n    });\n    win.on(\"closed\", () => {\n        win = null;\n        if (winCloseTimer) {\n            clearTimeout(winCloseTimer);\n        }\n        setTimeout(() => {\n            if (toastMsgQueue.length > 0) {\n                const item = toastMsgQueue.shift();\n                makeToast(item.msg, item.options);\n            }\n        }, 0);\n    });\n    winCloseTimer = setTimeout(() => {\n        winCloseTimer = null;\n        if (!win) return;\n        win.close();\n    }, options.duration);\n};\n"
  },
  {
    "path": "electron/mapi/config/index.ts",
    "content": "import { callHandleFromMainOrRender } from \"../env\";\n\nconst all = async () => {\n    return callHandleFromMainOrRender(\"config:all\");\n};\n\nconst get = async (key: string, defaultValue: any = null) => {\n    return callHandleFromMainOrRender(\"config:get\", key, defaultValue);\n};\nconst set = async (key: string, value: any) => {\n    await callHandleFromMainOrRender(\"config:set\", key, value);\n};\n\nconst allEnv = async () => {\n    return callHandleFromMainOrRender(\"config:allEnv\");\n};\n\nconst getEnv = async (key: string, defaultValue: any = null) => {\n    return callHandleFromMainOrRender(\"config:getEnv\", key, defaultValue);\n};\n\nconst setEnv = async (key: string, value: any) => {\n    await callHandleFromMainOrRender(\"config:setEnv\", key, value);\n};\n\nexport const ConfigIndex = {\n    all,\n    get,\n    set,\n    allEnv,\n    getEnv,\n    setEnv,\n};\n"
  },
  {
    "path": "electron/mapi/config/main.ts",
    "content": "import path from \"node:path\";\nimport { AppEnv } from \"../env\";\nimport fs from \"node:fs\";\nimport { ipcMain } from \"electron\";\nimport { Events } from \"../event/main\";\n\nlet data = null;\nlet dataEnv = {};\n\nconst userDataRoot = () => {\n    return path.join(AppEnv.userData, \"config.json\");\n};\n\nconst dataRoot = () => {\n    return path.join(AppEnv.dataRoot, \"config.json\");\n};\n\nconst filePath = () => {\n    if (fs.existsSync(userDataRoot())) {\n        return userDataRoot();\n    }\n    return dataRoot();\n};\n\nconst load = () => {\n    try {\n        let json = fs.readFileSync(filePath()).toString();\n        json = JSON.parse(json);\n        data = json || {};\n    } catch (e) {\n        data = {};\n    }\n};\n\nconst loadIfNeed = () => {\n    if (data === null) {\n        load();\n    }\n};\n\nconst save = () => {\n    fs.writeFileSync(filePath(), JSON.stringify(data, null, 4));\n};\n\nconst all = async () => {\n    loadIfNeed();\n    return data;\n};\n\nconst get = async (key: string, defaultValue: any = null) => {\n    loadIfNeed();\n    if (!(key in data)) {\n        data[key] = defaultValue;\n        save();\n    }\n    return data[key];\n};\n\nconst set = async (key: string, value: any) => {\n    loadIfNeed();\n    data[key] = value;\n    save();\n};\n\nconst allEnv = async () => {\n    return dataEnv;\n};\n\nconst getEnv = async (key: string, defaultValue: any = null) => {\n    if (!(key in dataEnv)) {\n        dataEnv[key] = defaultValue;\n    }\n    return dataEnv[key];\n};\n\nconst setEnv = async (key: string, value: any) => {\n    dataEnv[key] = value;\n};\n\nipcMain.handle(\"config:all\", async (_) => {\n    return await all();\n});\nipcMain.handle(\n    \"config:get\",\n    async (_, key: string, defaultValue: any = null) => {\n        return await get(key, defaultValue);\n    },\n);\nipcMain.handle(\"config:set\", async (_, key: string, value: any) => {\n    const res = await set(key, value);\n    Events.broadcast(\"ConfigChange\", { key, value });\n    return res;\n});\n\nipcMain.handle(\"config:allEnv\", async (_) => {\n    return await allEnv();\n});\n\nipcMain.handle(\n    \"config:getEnv\",\n    async (_, key: string, defaultValue: any = null) => {\n        return await getEnv(key, defaultValue);\n    },\n);\n\nipcMain.handle(\"config:setEnv\", async (_, key: string, value: any) => {\n    const res = await setEnv(key, value);\n    Events.broadcast(\"ConfigEnvChange\", { key, value });\n    return res;\n});\n\nexport const ConfigMain = {\n    all,\n    get,\n    set,\n    allEnv,\n    getEnv,\n    setEnv,\n};\n\nexport default ConfigMain;\n"
  },
  {
    "path": "electron/mapi/config/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\n\nconst all = async () => {\n    return ipcRenderer.invoke(\"config:all\");\n};\n\nconst get = async (key: string, defaultValue: any = null) => {\n    return ipcRenderer.invoke(\"config:get\", key, defaultValue);\n};\n\nconst set = async (key: string, value: any) => {\n    return ipcRenderer.invoke(\"config:set\", key, value);\n};\n\nconst allEnv = async () => {\n    return ipcRenderer.invoke(\"config:allEnv\");\n};\n\nconst getEnv = async (key: string, defaultValue: any = null) => {\n    return ipcRenderer.invoke(\"config:getEnv\", key, defaultValue);\n};\n\nconst setEnv = async (key: string, value: any) => {\n    return ipcRenderer.invoke(\"config:setEnv\", key, value);\n};\n\nexport default {\n    all,\n    get,\n    set,\n    allEnv,\n    getEnv,\n    setEnv,\n};\n"
  },
  {
    "path": "electron/mapi/db/db.ts",
    "content": ""
  },
  {
    "path": "electron/mapi/db/main.ts",
    "content": "import sqlite3, { Database } from \"better-sqlite3\";\nimport path from \"node:path\";\nimport migration from \"./migration\";\nimport { AppEnv } from \"../env\";\nimport { Log } from \"../log/main\";\nimport { ipcMain } from \"electron\";\nimport fs from \"node:fs\";\nimport { Files } from \"../file/main\";\n\nlet dbPath: string | null = null;\nlet dbConn: Database | null = null;\nlet dbSuccess = false;\n\nconst db = {\n    /**\n     * 检查数据库连接是否已初始化\n     * @throws {string} 如果数据库未初始化则抛出异常\n     */\n    _check() {\n        if (!dbSuccess) {\n            throw \"DBNotInitialized\";\n        }\n    },\n    /**\n     * 执行SQL语句（无返回值）\n     * @param {string} sql - SQL语句\n     * @param {any[]} params - 参数数组\n     * @returns {Promise<void>}\n     */\n    async execute(sql: string, params: any = []): Promise<void> {\n        db._check();\n        try {\n            dbConn.prepare(sql).run(...params);\n        } catch (err) {\n            throw err;\n        }\n    },\n    /**\n     * 插入数据并返回插入的行ID\n     * @param {string} sql - SQL语句\n     * @param {any[]} params - 参数数组\n     * @returns {Promise<string | number>} 插入的行ID\n     */\n    async insert(sql: string, params: any = []): Promise<string | number> {\n        db._check();\n        try {\n            const result = dbConn.prepare(sql).run(...params);\n            return result.lastInsertRowid;\n        } catch (err) {\n            throw err;\n        }\n    },\n    /**\n     * 查询单行数据\n     * @param {string} sql - SQL语句\n     * @param {any[]} params - 参数数组\n     * @returns {Promise<any>} 查询结果\n     */\n    async first(sql: string, params: any = []): Promise<any> {\n        db._check();\n        try {\n            return dbConn.prepare(sql).get(...params);\n        } catch (err) {\n            throw err;\n        }\n    },\n    /**\n     * 查询多行数据\n     * @param {string} sql - SQL语句\n     * @param {any[]} params - 参数数组\n     * @returns {Promise<any[]>} 查询结果数组\n     */\n    async select(sql: string, params: any = []): Promise<any[]> {\n        db._check();\n        try {\n            return dbConn.prepare(sql).all(...params);\n        } catch (err) {\n            throw err;\n        }\n    },\n    /**\n     * 更新数据并返回影响的行数\n     * @param {string} sql - SQL语句\n     * @param {any[]} params - 参数数组\n     * @returns {Promise<number>} 影响的行数\n     */\n    async update(sql: string, params: any = []): Promise<number> {\n        db._check();\n        try {\n            const result = dbConn.prepare(sql).run(...params);\n            return result.changes;\n        } catch (err) {\n            throw err;\n        }\n    },\n    /**\n     * 删除数据并返回影响的行数\n     * @param {string} sql - SQL语句\n     * @param {any[]} params - 参数数组\n     * @returns {Promise<number>} 影响的行数\n     */\n    async delete(sql: string, params: any = []): Promise<number> {\n        db._check();\n        try {\n            const result = dbConn.prepare(sql).run(...params);\n            return result.changes;\n        } catch (err) {\n            throw err;\n        }\n    },\n};\n\nconst migrate = async () => {\n    await db.execute(`CREATE TABLE IF NOT EXISTS migrate\n                      (\n                          id\n                          INTEGER\n                          PRIMARY\n                          KEY,\n                          version\n                          INTEGER\n                      )`);\n    for (const version of migration.versions) {\n        const result = await db.first(\n            `SELECT *\n             FROM migrate\n             WHERE version = ?`,\n            [version.version],\n        );\n        if (!result) {\n            Log.info(`DB.Migrate`, { version: version.version });\n            await version.up(db);\n            await db.execute(\n                `INSERT INTO migrate (version)\n                 VALUES (?)`,\n                [version.version],\n            );\n        }\n    }\n};\n\n/**\n * 初始化数据库连接\n * @returns {Promise<void>}\n */\nconst init = async () => {\n    dbPath = path.join(AppEnv.dataRoot, \"database.db\");\n    const userDbPath = path.join(AppEnv.userData, \"database.db\");\n    if (fs.existsSync(userDbPath)) {\n        dbPath = userDbPath;\n    }\n    try {\n        dbConn = new sqlite3(dbPath);\n        dbSuccess = true;\n        await migrate();\n        Log.info(\"Database connected successfully\");\n    } catch (err) {\n        Log.error(\"DBConnect SQLite database failed:\", err.message);\n        throw err;\n    }\n};\n\nipcMain.handle(\"db:execute\", (event, sql: string, params: any) => {\n    return db.execute(sql, params);\n});\nipcMain.handle(\"db:insert\", (event, sql: string, params: any) => {\n    return db.insert(sql, params);\n});\nipcMain.handle(\"db:first\", (event, sql: string, params: any) => {\n    return db.first(sql, params);\n});\nipcMain.handle(\"db:select\", (event, sql: string, params: any) => {\n    return db.select(sql, params);\n});\nipcMain.handle(\"db:update\", (event, sql: string, params: any) => {\n    return db.update(sql, params);\n});\nipcMain.handle(\"db:delete\", (event, sql: string, params: any) => {\n    return db.delete(sql, params);\n});\n\nexport const DBMain = {\n    init,\n    execute: db.execute,\n    insert: db.insert,\n    first: db.first,\n    select: db.select,\n    update: db.update,\n    delete: db.delete,\n};\n\nexport default DBMain;\n"
  },
  {
    "path": "electron/mapi/db/migration.ts",
    "content": "const versions = [\n    {\n        version: 0,\n        up: async (db: DB) => {\n            // await db.execute(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`);\n            // console.log('db.insert', await db.insert(`INSERT INTO users (name, email) VALUES (?, ?)`,['Alice', 'alice@example.com']));\n            // console.log('db.select', await db.select(`SELECT * FROM users`));\n            // console.log('db.first', await db.first(`SELECT * FROM users`));\n        },\n    },\n    {\n        version: 1,\n        up: async (db: DB) => {\n            await db.execute(`CREATE TABLE IF NOT EXISTS kvdb_data\n                              (\n                                  id           TEXT PRIMARY KEY,\n                                  cloudVersion INTEGER,\n                                  version      INTEGER,\n                                  isDeleted    INTEGER,\n                                  name         TEXT\n                              )`);\n            await db.execute(`CREATE INDEX IF NOT EXISTS idx_kvdb_data_name\n                ON kvdb_data (name)\n            `);\n        },\n    },\n];\n\nexport default {\n    versions,\n};\n"
  },
  {
    "path": "electron/mapi/db/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\n\nconst init = () => {};\n\nconst execute = async (sql: string, params: any = []) => {\n    return ipcRenderer.invoke(\"db:execute\", sql, params);\n};\n\nconst insert = async (sql: string, params: any = []) => {\n    return ipcRenderer.invoke(\"db:insert\", sql, params);\n};\n\nconst first = async (sql: string, params: any = []) => {\n    return ipcRenderer.invoke(\"db:first\", sql, params);\n};\n\nconst select = async (sql: string, params: any = []) => {\n    return ipcRenderer.invoke(\"db:select\", sql, params);\n};\n\nconst update = async (sql: string, params: any = []) => {\n    return ipcRenderer.invoke(\"db:update\", sql, params);\n};\n\nconst deletes = async (sql: string, params: any = []) => {\n    return ipcRenderer.invoke(\"db:delete\", sql, params);\n};\n\nexport default {\n    init,\n    execute,\n    insert,\n    first,\n    select,\n    update,\n    delete: deletes,\n};\n"
  },
  {
    "path": "electron/mapi/db/type.d.ts",
    "content": "type DB = {\n    execute(sql: string, params?: any): Promise<any>;\n    insert(sql: string, params?: any): Promise<any>;\n    first(sql: string, params?: any): Promise<any>;\n    select(sql: string, params?: any): Promise<any>;\n    update(sql: string, params?: any): Promise<any>;\n    delete(sql: string, params?: any): Promise<any>;\n};\n"
  },
  {
    "path": "electron/mapi/env.ts",
    "content": "import electron, { BrowserWindow } from \"electron\";\nimport { Log } from \"./log\";\n\nexport const AppEnv = {\n    isInit: false,\n    appRoot: null as string,\n    appData: null as string,\n    userData: null as string,\n    dataRoot: null as string,\n};\n\nexport const AppRuntime = {\n    fileHubRoot: null as string,\n    splashWindow: null as BrowserWindow,\n    mainWindow: null as BrowserWindow,\n    fastPanelWindow: null as BrowserWindow,\n    windows: {} as Record<string, BrowserWindow>,\n};\n\nexport const waitAppEnvReady = async () => {\n    while (!AppEnv.isInit) {\n        await new Promise((resolve) => {\n            setTimeout(resolve, 1000);\n        });\n    }\n};\n\nexport const callHandleFromMainOrRender = async (name: string, ...args) => {\n    if (electron.ipcRenderer) {\n        return electron.ipcRenderer.invoke(name, ...args);\n    } else {\n        // @ts-ignore\n        const func = electron.ipcMain._invokeHandlers.get(name);\n        if (func) {\n            return func(...args);\n        } else {\n            Log.error(`No handler found for ${name}`);\n            return null;\n        }\n    }\n};\n"
  },
  {
    "path": "electron/mapi/event/main.ts",
    "content": "import { AppRuntime } from \"../env\";\nimport { ipcMain, WebContents } from \"electron\";\nimport { StrUtil } from \"../../lib/util\";\nimport { ManagerWindow } from \"../manager/window\";\n\nconst init = async () => {};\n\ntype NameType = \"main\" | \"fastPanel\" | string | WebContents;\ntype EventType = \"APP_READY\" | \"CALL_PAGE\" | \"CHANNEL\" | \"BROADCAST\";\ntype BroadcastType =\n    | \"ConfigChange\"\n    | \"ConfigEnvChange\"\n    | \"UserChange\"\n    | \"DarkModeChange\"\n    | \"HotkeyWatch\"\n    | \"Notice\"\n    | \"MonitorEvent\";\n\nconst broadcast = (\n    type: BroadcastType,\n    data: any,\n    option?: {\n        limit?: boolean;\n        scopes?: string[];\n        pages?: string[];\n    },\n) => {\n    data = data || {};\n    option = Object.assign(\n        {\n            limit: false,\n            scopes: [],\n            pages: [],\n        },\n        option,\n    );\n    if (option.pages.length > 0) {\n        for (const p of option.pages) {\n            send(p, \"BROADCAST\", { type, data });\n        }\n    } else {\n        if (!option.limit || option.scopes.includes(\"main\")) {\n            send(\"main\", \"BROADCAST\", { type, data });\n        }\n        if (!option.limit || option.scopes.includes(\"pages\")) {\n            for (let name in AppRuntime.windows) {\n                send(name, \"BROADCAST\", { type, data });\n            }\n        }\n    }\n    if (!option.limit || option.scopes.includes(\"fastPanel\")) {\n        send(\"fastPanel\", \"BROADCAST\", { type, data });\n    }\n    if (!option.limit || option.scopes.includes(\"views\")) {\n        for (const view of ManagerWindow.listBrowserViews()) {\n            view.webContents.send(\"MAIN_PROCESS_MESSAGE\", {\n                id: StrUtil.randomString(32),\n                type: \"BROADCAST\",\n                data: { type, data },\n            });\n        }\n    }\n    if (!option.limit || option.scopes.includes(\"detachWindows\")) {\n        for (const win of ManagerWindow.listDetachWindows()) {\n            win.webContents.send(\"MAIN_PROCESS_MESSAGE\", {\n                id: StrUtil.randomString(32),\n                type: \"BROADCAST\",\n                data: { type, data },\n            });\n        }\n    }\n};\n\nconst sendRaw = (\n    webContents: any,\n    type: EventType,\n    data: any = {},\n    id?: string,\n): boolean => {\n    id = id || StrUtil.randomString(32);\n    const payload = { id, type, data };\n    webContents.send(\"MAIN_PROCESS_MESSAGE\", payload);\n    return true;\n};\n\nconst send = (\n    name: NameType,\n    type: EventType,\n    data: any = {},\n    id?: string,\n): boolean => {\n    id = id || StrUtil.randomString(32);\n    const payload = { id, type, data };\n    if (typeof name !== \"string\") {\n        (name as WebContents).send(\"MAIN_PROCESS_MESSAGE\", payload);\n        return true;\n    }\n    if (name === \"main\") {\n        if (!AppRuntime.mainWindow) {\n            return false;\n        }\n        // console.log('send', payload)\n        AppRuntime.mainWindow?.webContents.send(\n            \"MAIN_PROCESS_MESSAGE\",\n            payload,\n        );\n    } else if (name === \"fastPanel\") {\n        if (!AppRuntime.fastPanelWindow) {\n            return false;\n        }\n        AppRuntime.fastPanelWindow?.webContents.send(\n            \"MAIN_PROCESS_MESSAGE\",\n            payload,\n        );\n    } else {\n        if (!AppRuntime.windows[name]) {\n            return false;\n        }\n        AppRuntime.windows[name]?.webContents.send(\n            \"MAIN_PROCESS_MESSAGE\",\n            payload,\n        );\n    }\n    return true;\n};\n\nipcMain.handle(\n    \"event:send\",\n    async (_, name: NameType, type: EventType, data: any) => {\n        send(name, type, data);\n    },\n);\n\nconst callPage = async (\n    name: NameType,\n    type: string,\n    data: any,\n    option?: {\n        waitReadyTimeout?: number;\n        timeout?: number;\n    },\n): Promise<{\n    code: number;\n    msg: string;\n    data?: any;\n}> => {\n    option = Object.assign(\n        {\n            waitReadyTimeout: 10 * 1000,\n            timeout: 60 * 1000,\n        },\n        option,\n    );\n    return new Promise((resolve, reject) => {\n        const id = StrUtil.randomString(32);\n        const timer = setTimeout(() => {\n            ipcMain.removeListener(listenerKey, listener);\n            resolve({ code: -1, msg: \"timeout\" });\n        }, option.timeout);\n        const listener = (_, result) => {\n            clearTimeout(timer);\n            resolve(result);\n            return true;\n        };\n        const listenerKey = \"event:callPage:\" + id;\n        ipcMain.once(listenerKey, listener);\n        const payload = {\n            type,\n            data,\n            option: {\n                waitReadyTimeout: option.waitReadyTimeout,\n            },\n        };\n        if (!send(name, \"CALL_PAGE\", payload, id)) {\n            clearTimeout(timer);\n            ipcMain.removeListener(listenerKey, listener);\n            resolve({ code: -1, msg: \"send failed\" });\n        }\n    });\n};\n\nipcMain.handle(\n    \"event:callPage\",\n    async (_, name: string, type: string, data: any, option?: {}) => {\n        return callPage(name, type, data, option);\n    },\n);\n\nlet onChannelIsListen = false;\nlet channelOnCallback = {};\n\nconst sendChannel = (channel: string, data: any) => {\n    send(\"main\", \"CHANNEL\", { channel, data });\n};\n\nconst onChannel = (channel: string, callback: (data: any) => void) => {\n    if (!channelOnCallback[channel]) {\n        channelOnCallback[channel] = [];\n    }\n    channelOnCallback[channel].push(callback);\n    if (!onChannelIsListen) {\n        onChannelIsListen = true;\n        ipcMain.handle(\"event:channelSend\", (event, channel_, data) => {\n            if (channelOnCallback[channel_]) {\n                channelOnCallback[channel_].forEach(\n                    (callback: (data: any) => void) => {\n                        callback(data);\n                    },\n                );\n            }\n        });\n    }\n};\n\nconst offChannel = (channel: string, callback: (data: any) => void) => {\n    if (channelOnCallback[channel]) {\n        channelOnCallback[channel] = channelOnCallback[channel].filter(\n            (item: (data: any) => void) => {\n                return item !== callback;\n            },\n        );\n    }\n    if (channelOnCallback[channel].length === 0) {\n        delete channelOnCallback[channel];\n    }\n};\n\nexport default {\n    init,\n    send,\n};\n\nexport const Events = {\n    broadcast,\n    send,\n    sendRaw,\n    sendChannel,\n    callPage,\n    onChannel,\n    offChannel,\n};\n"
  },
  {
    "path": "electron/mapi/event/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\n\nconst init = () => {};\n\nconst send = (name: string, type: string, data: any = {}) => {\n    return ipcRenderer.invoke(\"event:send\", name, type, data).then();\n};\n\nconst callPage = async (name: string, type: string, data: any, option: any) => {\n    return ipcRenderer.invoke(\"event:callPage\", name, type, data, option);\n};\n\nconst channelSend = async (channel: string, data: any) => {\n    return ipcRenderer.invoke(\"event:channelSend\", channel, data);\n};\n\nexport default {\n    init,\n    send,\n    callPage,\n    channelSend,\n};\n"
  },
  {
    "path": "electron/mapi/file/index.ts",
    "content": "import fs, { createWriteStream } from \"node:fs\";\nimport path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { ReadableStream } from \"node:stream/web\";\nimport { EncodeUtil, StrUtil, TimeUtil } from \"../../lib/util\";\nimport Apps from \"../app\";\nimport { ConfigIndex } from \"../config\";\nimport { AppEnv, waitAppEnvReady } from \"../env\";\nimport { Log } from \"../log\";\nimport electron from \"electron\";\nimport { finished } from \"stream/promises\";\n\nconst nodePath = path;\n\nconst toNodeReadableStream = (stream: any) => {\n    if (stream instanceof ReadableStream) {\n        // 已经是 Node.js 版本的 WHATWG ReadableStream\n        return Readable.fromWeb(stream);\n    }\n    if (typeof stream.getReader === \"function\") {\n        // 浏览器版本 → 包装成 Node.js 兼容的\n        const nodeStream = new ReadableStream({\n            async pull(controller) {\n                const reader = stream.getReader();\n                while (true) {\n                    const { done, value } = await reader.read();\n                    if (done) break;\n                    controller.enqueue(value);\n                }\n                controller.close();\n            },\n        });\n        return Readable.fromWeb(nodeStream);\n    }\n    throw new Error(\"Unsupported stream type\");\n};\n\nconst toWebReadableStream = (stream: any) => {\n    const reader = stream[Symbol.asyncIterator]();\n    return new window.ReadableStream({\n        async pull(controller) {\n            const { value, done } = await reader.next();\n            if (done) {\n                controller.close();\n            } else {\n                controller.enqueue(value);\n            }\n        },\n    });\n};\n\nconst root = () => {\n    return AppEnv.dataRoot;\n};\n\nconst absolutePath = (path: string) => {\n    return `ABS://${path}`;\n};\n\nconst fullPath = async (path: string) => {\n    await waitAppEnvReady();\n    if (path.startsWith(\"ABS://\")) {\n        return path.replace(/^ABS:\\/\\//, \"\");\n    }\n    return nodePath.join(root(), path);\n};\n\nconst exists = async (\n    path: string,\n    option?: { isDataPath?: boolean },\n): Promise<boolean> => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    return new Promise((resolve, reject) => {\n        fs.stat(fp, (err, stat) => {\n            if (err) {\n                resolve(false);\n            } else {\n                resolve(true);\n            }\n        });\n    });\n};\n\nconst isDirectory = async (path: string, option?: { isDataPath?: boolean }) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        return false;\n    }\n    return fs.statSync(fp).isDirectory();\n};\n\nconst mkdir = async (path: string, option?: { isDataPath?: boolean }) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        fs.mkdirSync(fp, { recursive: true });\n    }\n};\n\nconst list = async (path: string, option?: { isDataPath?: boolean }) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        return [];\n    }\n    const files = fs.readdirSync(fp);\n    return files.map((file) => {\n        const stat = fs.statSync(nodePath.join(fp, file));\n        let f = {\n            name: file,\n            pathname: nodePath.join(fp, file),\n            isDirectory: stat.isDirectory(),\n            size: stat.size,\n            lastModified: stat.mtimeMs,\n        };\n        return f;\n    });\n};\n\nconst listAll = async (path: string, option?: { isDataPath?: boolean }) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        return [];\n    }\n    const listDirectory = (path: string, basePath: string = \"\") => {\n        let files = [];\n        const list = fs.readdirSync(path);\n        for (let file of list) {\n            const stat = fs.statSync(nodePath.join(path, file));\n            let fPath = nodePath.join(basePath, file);\n            fPath = fPath.replace(/\\\\/g, \"/\");\n            let f = {\n                name: file,\n                path: fPath,\n                isDirectory: stat.isDirectory(),\n                size: stat.size,\n                lastModified: stat.mtimeMs,\n            };\n            if (f.isDirectory) {\n                files = files.concat(\n                    listDirectory(nodePath.join(path, file), f.path),\n                );\n                continue;\n            }\n            files.push(f);\n        }\n        return files;\n    };\n    return listDirectory(fp);\n};\n\nconst write = async (\n    path: string,\n    data: any,\n    option?: { isDataPath?: boolean },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    const fullPathDir = nodePath.dirname(fp);\n    if (!fs.existsSync(fullPathDir)) {\n        fs.mkdirSync(fullPathDir, { recursive: true });\n    }\n    if (typeof data === \"string\") {\n        data = {\n            content: data,\n        };\n    }\n    const f = fs.openSync(fp, \"w\");\n    fs.writeSync(f, data.content);\n    fs.closeSync(f);\n};\n\nconst writeStream = async (\n    path: string,\n    data: any,\n    option?: { isDataPath?: boolean },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    const fullPathDir = nodePath.dirname(fp);\n    if (!fs.existsSync(fullPathDir)) {\n        fs.mkdirSync(fullPathDir, { recursive: true });\n    }\n    if (electron.ipcRenderer) {\n        data = toNodeReadableStream(data);\n    }\n    const fileStream = createWriteStream(fp);\n    data.pipe(fileStream);\n    await finished(fileStream);\n};\n\nconst writeBuffer = async (\n    path: string,\n    data: any,\n    option?: { isDataPath?: boolean },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    const fullPathDir = nodePath.dirname(fp);\n    if (!fs.existsSync(fullPathDir)) {\n        fs.mkdirSync(fullPathDir, { recursive: true });\n    }\n    const f = fs.openSync(fp, \"w\");\n    fs.writeSync(f, data);\n    fs.closeSync(f);\n};\n\nconst read = async (\n    path: string,\n    option?: {\n        isDataPath?: boolean;\n        encoding?: string;\n    },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n            encoding: \"utf8\",\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        return null;\n    }\n    const f = fs.openSync(fp, \"r\");\n    const content = fs.readFileSync(f, {\n        encoding: option.encoding as BufferEncoding,\n    });\n    fs.closeSync(f);\n    return content;\n};\n\nconst readBuffer = async (\n    path: string,\n    option?: { isDataPath?: boolean },\n): Promise<Buffer> => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        return null;\n    }\n    return new Promise((resolve, reject) => {\n        fs.readFile(fp, (err, data) => {\n            if (err) {\n                reject(err);\n                return;\n            }\n            resolve(data);\n        });\n    });\n};\n\nconst readStream = async (path: string, option?: { isDataPath?: boolean }) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        throw `FileNotFound: ${fp}`;\n    }\n    const stream = fs.createReadStream(fp);\n    if (electron.ipcRenderer) {\n        return toWebReadableStream(stream);\n    }\n    return stream;\n};\n\nconst readLine = async (\n    path: string,\n    callback: (line: string) => void,\n    option?: {\n        isDataPath?: boolean;\n    },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!fs.existsSync(fp)) {\n        return;\n    }\n    return new Promise((resolve, reject) => {\n        const f = fs.createReadStream(fp);\n        let remaining = \"\";\n        f.on(\"data\", (chunk) => {\n            remaining += chunk;\n            let index = remaining.indexOf(\"\\n\");\n            let last = 0;\n            while (index > -1) {\n                let line = remaining.substring(last, index);\n                last = index + 1;\n                callback(line);\n                index = remaining.indexOf(\"\\n\", last);\n            }\n            remaining = remaining.substring(last);\n        });\n        f.on(\"end\", () => {\n            if (remaining.length > 0) {\n                callback(remaining);\n            }\n            resolve(undefined);\n        });\n    });\n};\n\nconst clean = async (paths: string[], option?: { isDataPath?: boolean }) => {\n    if (!paths || !Array.isArray(paths) || paths.length === 0) {\n        return;\n    }\n    for (const path of paths) {\n        try {\n            await deletes(path, option);\n        } catch (e) {\n            Log.error(`CleanError: ${path}`, e);\n        }\n    }\n};\n\nconst deletes = async (\n    path: string,\n    option?: { isDataPath?: boolean },\n): Promise<void> => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (!(await exists(fp, { isDataPath: false }))) {\n        return;\n    }\n    return new Promise((resolve, reject) => {\n        fs.stat(fp, (err, stat) => {\n            if (err) {\n                reject(err);\n                return;\n            }\n            if (stat.isDirectory()) {\n                fs.rmdir(fp, { recursive: true }, (err) => {\n                    if (err) {\n                        reject(err);\n                        return;\n                    }\n                    resolve(undefined);\n                });\n            } else {\n                fs.unlink(fp, (err) => {\n                    if (err) {\n                        reject(err);\n                        return;\n                    }\n                    resolve(undefined);\n                });\n            }\n        });\n    });\n};\nconst rename = async (\n    pathOld: string,\n    pathNew: string,\n    option?: {\n        isDataPath?: boolean;\n        overwrite?: boolean;\n    },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n            overwrite: false,\n        },\n        option,\n    );\n    let fullPathOld = pathOld;\n    let fullPathNew = pathNew;\n    if (option.isDataPath) {\n        fullPathOld = await fullPath(pathOld);\n        fullPathNew = await fullPath(pathNew);\n    }\n    if (!fs.existsSync(fullPathOld)) {\n        throw `Rename.FileNotFound - ${fullPathOld}`;\n    }\n    if (fs.existsSync(fullPathNew)) {\n        if (!option.overwrite) {\n            throw new Error(`FileAlreadyExists:${fullPathNew}`);\n        }\n        fs.unlinkSync(fullPathNew);\n    }\n    const dir = nodePath.dirname(fullPathNew);\n    if (!fs.existsSync(dir)) {\n        fs.mkdirSync(dir, { recursive: true });\n    }\n    let success = false;\n    try {\n        fs.renameSync(fullPathOld, fullPathNew);\n        success = true;\n    } catch (e) {}\n    if (!success) {\n        // cross-device link not permitted, rename\n        fs.copyFileSync(fullPathOld, fullPathNew);\n        fs.unlinkSync(fullPathOld);\n    }\n};\n\nconst copy = async (\n    pathOld: string,\n    pathNew: string,\n    option?: {\n        isDataPath?: boolean;\n        overwrite?: boolean;\n    },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n            overwrite: false,\n        },\n        option,\n    );\n    let fullPathOld = pathOld;\n    let fullPathNew = pathNew;\n    if (option.isDataPath) {\n        fullPathOld = await fullPath(pathOld);\n        fullPathNew = await fullPath(pathNew);\n    }\n    if (!fs.existsSync(fullPathOld)) {\n        throw `Copy.FileNotFound - ${fullPathOld}`;\n    }\n    if (fs.existsSync(fullPathNew)) {\n        if (option.overwrite) {\n            await deletes(fullPathNew, { isDataPath: false });\n        } else {\n            throw `Copy.FileAlreadyExists - ${fullPathNew}`;\n        }\n    }\n    // console.log('copy', fullPathOld, fullPathNew)\n    const dir = nodePath.dirname(fullPathNew);\n    if (!fs.existsSync(dir)) {\n        fs.mkdirSync(dir, { recursive: true });\n    }\n    fs.copyFileSync(fullPathOld, fullPathNew);\n};\n\nconst hubRootDefault = async () => {\n    await waitAppEnvReady();\n    return path.join(root(), \"hub\");\n};\n\nconst hubRoot = async (): Promise<string> => {\n    const hubDirDefault = await hubRootDefault();\n    let hubDir = await ConfigIndex.get(\"hubRoot\", \"\");\n    if (!hubDir) {\n        hubDir = hubDirDefault;\n    }\n    if (!fs.existsSync(hubDir)) {\n        fs.mkdirSync(hubDir, { recursive: true });\n    }\n    return hubDir;\n};\n\nconst _getHubSavePath = async (\n    hubRoot: string,\n    saveGroup: string,\n    savePath: string,\n    saveParam: {\n        [key: string]: any;\n    },\n    ext: string,\n    autoCreateDir: boolean = false,\n) => {\n    if (!saveGroup) {\n        saveGroup = \"file\";\n    }\n    if (!savePath) {\n        savePath = path.join(\n            saveGroup,\n            \"{year}{month}{day}\",\n            \"{hour}{minute}_{second}_{random}\",\n        );\n    }\n    savePath = savePath.replace(/\\\\/g, \"/\");\n    if (savePath.endsWith(`.${ext}`)) {\n        savePath = savePath.substring(0, savePath.length - ext.length - 1);\n    }\n    for (const key in saveParam) {\n        // only allow alphanumeric, Chinese characters, and hyphens\n        saveParam[key] = saveParam[key]\n            .toString()\n            .replace(/[^\\w\\u4e00-\\u9fa5\\-]/g, \"\");\n        // length limit\n        if (saveParam[key].length > 100) {\n            saveParam[key] = saveParam[key].substring(0, 100);\n        }\n    }\n    const param = {\n        year: TimeUtil.replacePattern(\"{year}\"),\n        month: TimeUtil.replacePattern(\"{month}\"),\n        day: TimeUtil.replacePattern(\"{day}\"),\n        hour: TimeUtil.replacePattern(\"{hour}\"),\n        minute: TimeUtil.replacePattern(\"{minute}\"),\n        second: TimeUtil.replacePattern(\"{second}\"),\n        random: StrUtil.randomString(32),\n        ...saveParam,\n    };\n    savePath = savePath.replace(/\\{(\\w+)\\}/g, (match, key) => {\n        return param[key] || key;\n    });\n    while (\n        await exists(path.join(hubRoot, savePath + `.${ext}`), {\n            isDataPath: false,\n        })\n    ) {\n        savePath = savePath + `-${StrUtil.randomString(3)}`;\n    }\n    if (autoCreateDir) {\n        const savePathFull = path.join(hubRoot, savePath);\n        const dir = nodePath.dirname(savePathFull);\n        if (!(await exists(dir, { isDataPath: false }))) {\n            fs.mkdirSync(dir, { recursive: true });\n        }\n    }\n    return `${savePath}.${ext}`;\n};\n\nconst hubDelete = async (\n    file: string,\n    option?: {\n        isDataPath?: boolean;\n        ignoreWhenNotInHub?: boolean;\n        tryLaterWhenFailed?: boolean;\n    },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n            ignoreWhenNotInHub: true,\n            tryLaterWhenFailed: true,\n        },\n        option,\n    );\n    let fp = file;\n    const hubRoot_ = await hubRoot();\n    if (option.isDataPath) {\n        fp = path.join(hubRoot_, file);\n    }\n    if (!(await isHubFile(fp))) {\n        if (option.ignoreWhenNotInHub) {\n            return;\n        }\n    }\n    if (!(await exists(fp, { isDataPath: false }))) {\n        throw `HubDelete.FileNotFound - ${fp}`;\n    }\n    const del = () => {\n        deletes(fp, { isDataPath: false }).catch((err) => {\n            if (option.tryLaterWhenFailed) {\n                setTimeout(del, 1000);\n            } else {\n                Log.error(`HubDelete.Error: ${fp}`, err);\n            }\n        });\n    };\n    del();\n};\n\nconst hubFullPath = async (file: string): Promise<string> => {\n    if (!file) {\n        throw \"HubSave.FilePathEmpty\";\n    }\n    const hubRoot_ = await hubRoot();\n    return path.join(hubRoot_, file);\n};\n\nconst hubFile = async (\n    ext: string,\n    option?: {\n        returnFullPath?: boolean;\n        autoCreateDir?: boolean;\n        saveGroup?: string;\n        savePath?: string;\n        savePathParam?: {\n            [key: string]: any;\n        };\n    },\n) => {\n    option = Object.assign(\n        {\n            returnFullPath: true,\n            autoCreateDir: true,\n            saveGroup: \"file\",\n            savePath: null,\n            savePathParam: {},\n        },\n        option,\n    );\n    if (!ext) {\n        throw \"HubSave.FilePathEmpty\";\n    }\n    const hubRoot_ = await hubRoot();\n    const savePath = await _getHubSavePath(\n        hubRoot_,\n        option.saveGroup,\n        option.savePath,\n        option.savePathParam,\n        ext,\n        option.autoCreateDir,\n    );\n    if (option.returnFullPath) {\n        return path.join(hubRoot_, savePath);\n    }\n    return savePath;\n};\n\nconst isHubFile = async (file: string) => {\n    const hubRoot_ = await hubRoot();\n    return inDir(file, hubRoot_);\n};\n\nconst hubSave = async (\n    file: string,\n    option?: {\n        ext?: string;\n        returnFullPath?: boolean;\n        ignoreWhenInHub?: boolean;\n        cleanOld?: boolean;\n        saveGroup?: string;\n        savePath?: string;\n        savePathParam?: {\n            [key: string]: any;\n        };\n    },\n) => {\n    option = Object.assign(\n        {\n            ext: null,\n            returnFullPath: true,\n            ignoreWhenInHub: false,\n            cleanOld: false,\n            saveGroup: \"file\",\n            savePath: null,\n            savePathParam: {},\n        },\n        option,\n    );\n    if (!file) {\n        throw \"HubSave.FilePathEmpty\";\n    }\n    if (!fs.existsSync(file)) {\n        throw `HubSave.FileNotFound - ${file}`;\n    }\n    if (!option.ext) {\n        option.ext = ext(file);\n    }\n    const hubRoot_ = await hubRoot();\n    if (option.ignoreWhenInHub) {\n        if (inDir(file, hubRoot_)) {\n            return file;\n        }\n    }\n    const savePath = await _getHubSavePath(\n        hubRoot_,\n        option.saveGroup,\n        option.savePath,\n        option.savePathParam,\n        option.ext,\n    );\n    const savePathFull = path.join(hubRoot_, savePath);\n    if (option.cleanOld) {\n        await rename(file, savePathFull, { isDataPath: false });\n        if (await exists(file, { isDataPath: false })) {\n            deletes(file, { isDataPath: false }).then();\n        }\n    } else {\n        await copy(file, savePathFull, {\n            isDataPath: false,\n        });\n    }\n    if (option.returnFullPath) {\n        return savePathFull;\n    }\n    return savePath;\n};\n\nconst hubSaveContent = async (\n    content: string,\n    option: {\n        ext: string;\n        returnFullPath?: boolean;\n        saveGroup?: string;\n        savePath?: string;\n        savePathParam?: {\n            [key: string]: any;\n        };\n    },\n) => {\n    option = Object.assign(\n        {\n            ext: null,\n            returnFullPath: true,\n            saveGroup: \"file\",\n            savePath: null,\n            savePathParam: {},\n        },\n        option,\n    );\n    const hubRoot_ = await hubRoot();\n    const savePath = await _getHubSavePath(\n        hubRoot_,\n        option.saveGroup,\n        option.savePath,\n        option.savePathParam,\n        option.ext,\n    );\n    const savePathFull = path.join(hubRoot_, savePath);\n    await write(savePathFull, content, { isDataPath: false });\n    if (option.returnFullPath) {\n        return savePathFull;\n    }\n    return savePath;\n};\n\nconst tempRoot = async () => {\n    await waitAppEnvReady();\n    const tempDir = path.join(root(), \"temp\");\n    if (!fs.existsSync(tempDir)) {\n        fs.mkdirSync(tempDir, { recursive: true });\n    }\n    return tempDir;\n};\n\nconst autoCleanTemp = async (keepDays: number = 7) => {\n    const root = await tempRoot();\n    if (!fs.existsSync(root)) {\n        return;\n    }\n    const files = fs.readdirSync(root);\n    const now = new Date();\n    for (const file of files) {\n        const filePath = path.join(root, file);\n        const stat = fs.statSync(filePath);\n        if (stat.isDirectory()) {\n            continue; // skip directories\n        }\n        const lastModified = new Date(stat.mtimeMs);\n        const diffDays = Math.floor(\n            (now.getTime() - lastModified.getTime()) / (1000 * 60 * 60 * 24),\n        );\n        if (diffDays >= keepDays) {\n            fs.unlinkSync(filePath);\n            Log.info(\"AutoCleanTemp.Clean\", filePath);\n        } else {\n            // console.log('AutoCleanTemp.Skip', filePath, diffDays);\n        }\n    }\n};\n\nconst tempName = async (\n    ext: string = \"tmp\",\n    prefix: string = \"file\",\n    suffix: string = \"\",\n): Promise<string> => {\n    const parts = [prefix, TimeUtil.timestampInMs(), StrUtil.randomString(32)];\n    if (suffix) {\n        parts.push(suffix);\n    }\n    const p = parts.join(\"_\");\n    return `${p}.${ext}`;\n};\n\nconst temp = async (\n    ext: string = \"tmp\",\n    prefix: string = \"file\",\n    suffix: string = \"\",\n): Promise<string> => {\n    const root = await tempRoot();\n    return path.join(root, await tempName(ext, prefix, suffix));\n};\n\nconst tempDir = async (prefix: string = \"dir\"): Promise<string> => {\n    const root = await tempRoot();\n    const p = [prefix, TimeUtil.timestampInMs(), StrUtil.randomString(32)].join(\n        \"_\",\n    );\n    const dir = path.join(root, p);\n    if (!fs.existsSync(dir)) {\n        fs.mkdirSync(dir, { recursive: true });\n    }\n    return dir;\n};\n\nconst watchText = async (\n    path: string,\n    callback: (data: {}) => void,\n    option?: {\n        isDataPath?: boolean;\n        limit?: number;\n    },\n): Promise<{\n    stop: Function;\n}> => {\n    if (!path) {\n        throw new Error(\"path is empty\");\n    }\n    option = Object.assign(\n        {\n            isDataPath: false,\n            limit: 0,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    let watcher = null;\n    let fd = null;\n    let isFirstReading = true;\n    let firstReadingLines = [];\n    const watchFileExists = () => {\n        if (fs.existsSync(fp)) {\n            watcher = null;\n            watchFileContent();\n            return;\n        }\n        watcher = setTimeout(() => {\n            watchFileExists();\n        }, 1000);\n    };\n    const watchFileContent = () => {\n        const CHUNK_SIZE = 16 * 1024;\n        const fd = fs.openSync(fp, \"r\");\n        let position = 0;\n        let lineNumber = 0;\n        let content = \"\";\n        const parseContentLine = () => {\n            while (true) {\n                const index = content.indexOf(\"\\n\");\n                if (index < 0) {\n                    break;\n                }\n                const line = content.substring(0, index);\n                content = content.substring(index + 1);\n                const lineItem = {\n                    num: lineNumber++,\n                    text: line,\n                };\n                if (option.limit > 0 && isFirstReading) {\n                    // 限制显示模式并且是第一次读取，暂时先不回调\n                    firstReadingLines.push(lineItem);\n                    while (firstReadingLines.length >= option.limit) {\n                        firstReadingLines.shift();\n                    }\n                } else {\n                    callback(lineItem);\n                }\n                // console.log('watchText.line', line, content)\n            }\n        };\n        const readChunk = () => {\n            const buf = new Buffer(CHUNK_SIZE);\n            const bytesRead = fs.readSync(fd, buf, 0, CHUNK_SIZE, position);\n            position += bytesRead;\n            content += buf.toString(\"utf8\", 0, bytesRead);\n            parseContentLine();\n            if (bytesRead < CHUNK_SIZE) {\n                isFirstReading = false;\n                if (firstReadingLines.length > 0) {\n                    firstReadingLines.forEach((lineItem) => {\n                        callback(lineItem);\n                    });\n                    firstReadingLines = [];\n                }\n                watcher = setTimeout(readChunk, 1000);\n            } else {\n                readChunk();\n            }\n        };\n        readChunk();\n    };\n    watchFileExists();\n    const stop = () => {\n        // console.log('watchText stop', fp)\n        if (fd) {\n            fs.closeSync(fd);\n        }\n        if (watcher) {\n            clearTimeout(watcher);\n        }\n    };\n    // console.log('watchText', fp)\n    return {\n        stop,\n    };\n};\n\nlet appendTextPathCached = null;\nlet appendTextStreamCached = null;\n\nconst appendText = async (\n    path: string,\n    data: any,\n    option?: { isDataPath?: boolean },\n) => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    if (fp !== appendTextPathCached) {\n        appendTextPathCached = fp;\n        if (appendTextStreamCached) {\n            appendTextStreamCached.end();\n            appendTextStreamCached = null;\n        }\n        const fullPathDir = nodePath.dirname(fp);\n        if (!fs.existsSync(fullPathDir)) {\n            fs.mkdirSync(fullPathDir, { recursive: true });\n        }\n        appendTextStreamCached = fs.createWriteStream(fp, { flags: \"a\" });\n    }\n    appendTextStreamCached.write(data);\n};\n\nconst download = async (\n    url: string,\n    path: string | null = null,\n    option?: {\n        isDataPath?: boolean;\n        userAgent?: string;\n        progress?: (percent: number, total: number) => void;\n    },\n): Promise<string> => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n            userAgent: Apps.getUserAgent(),\n            progress: null,\n        },\n        option,\n    );\n    if (!path) {\n        const ext = FileIndex.ext(url);\n        path = await temp(ext || \"bin\", \"download\");\n        option.isDataPath = false;\n    }\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    const fullPathDir = nodePath.dirname(fp);\n    if (!fs.existsSync(fullPathDir)) {\n        fs.mkdirSync(fullPathDir, { recursive: true });\n    }\n    const res = await fetch(url, {\n        method: \"GET\",\n        headers: {\n            \"User-Agent\": option.userAgent,\n        },\n    });\n    if (!res.ok) {\n        throw new Error(`DownloadError:${url}`);\n    }\n\n    const contentLength = res.headers.get(\"content-length\");\n    const totalSize = contentLength ? parseInt(contentLength, 10) : null;\n    let downloaded = 0;\n\n    let readableStream = toNodeReadableStream(res.body);\n    const fileStream = fs.createWriteStream(fp);\n    return new Promise((resolve, reject) => {\n        readableStream\n            .on(\"data\", (chunk) => {\n                // console.log('download.data', chunk.length)\n                downloaded += chunk.length;\n                if (totalSize) {\n                    option.progress &&\n                        option.progress(downloaded / totalSize, totalSize);\n                }\n                fileStream.write(chunk);\n            })\n            .on(\"end\", () => {\n                // console.log('download.end')\n                fileStream.end();\n                resolve(fp);\n            })\n            .on(\"error\", (err) => {\n                // console.log('download.error', err)\n                fileStream.close();\n                reject(err);\n            });\n    });\n};\n\n/**\n * get file extension from file path or url\n * @param path\n */\nconst ext = (path: string) => {\n    if (!path) {\n        return \"\";\n    }\n    if (path.startsWith(\"http://\") || path.startsWith(\"https://\")) {\n        // 处理 URL\n        const url = new URL(path);\n        path = url.pathname;\n    }\n    return nodePath.extname(path).replace(/^\\./, \"\");\n};\n\nconst stat = async (\n    path: string,\n    option?: { isDataPath?: boolean },\n): Promise<{\n    size: number;\n    isDirectory: boolean;\n    lastModified: number;\n}> => {\n    option = Object.assign(\n        {\n            isDataPath: false,\n        },\n        option,\n    );\n    let fp = path;\n    if (option.isDataPath) {\n        fp = await fullPath(path);\n    }\n    const stat = fs.statSync(fp);\n    return {\n        size: stat.size,\n        isDirectory: stat.isDirectory(),\n        lastModified: stat.mtimeMs,\n    };\n};\n\nconst textToName = (text: string, ext: string = \"\", maxLimit: number = 100) => {\n    if (text) {\n        // 转换为合法的文件名\n        text = text.replace(/[\\\\\\/\\:\\*\\?\\\"\\<\\>\\|]/g, \"\");\n        text = text.replace(/[\\r\\n]/g, \"\");\n        text = text.replace(/\\s+/g, \"\");\n        text = text.substring(0, maxLimit);\n    }\n    if (!text) {\n        text = \"EMPTY\";\n    }\n    if (!ext) {\n        return text;\n    }\n    return `${text}.${ext}`;\n};\n\nconst inDir = (path: string, dir: string) => {\n    if (!path || !dir) {\n        return false;\n    }\n    path = path.replace(/\\\\/g, \"/\");\n    dir = dir.replace(/\\\\/g, \"/\");\n    if (path === dir) {\n        return true;\n    }\n    return path.startsWith(dir);\n};\n\nconst pathToName = (\n    path: string,\n    includeExt: boolean = true,\n    maxLimit: number = 100,\n) => {\n    if (!path) {\n        return \"\";\n    }\n    path = path.replace(/\\\\/g, \"/\");\n    const parts = path.split(\"/\");\n    const nameWithExt = parts[parts.length - 1];\n    const nameParts = nameWithExt.split(\".\");\n    let ext = \"\";\n    if (nameParts.length > 1) {\n        ext = \".\" + nameParts.pop();\n    }\n    if (!includeExt) {\n        ext = \"\";\n    }\n    let result = nameParts.join(\".\");\n    maxLimit -= ext.length;\n    if (maxLimit > 0 && result.length > maxLimit) {\n        result = result.substring(0, maxLimit);\n    }\n    if (!result) {\n        result = \"EMPTY\";\n    }\n    return `${result}${ext}`;\n};\n\nconst _sortObjectDeep = (obj: any): any => {\n    if (Array.isArray(obj)) {\n        return obj.map(_sortObjectDeep);\n    } else if (obj && typeof obj === \"object\") {\n        return Object.keys(obj)\n            .sort()\n            .reduce((acc, key) => {\n                acc[key] = _sortObjectDeep(obj[key]);\n                return acc;\n            }, {} as any);\n    }\n    return obj;\n};\n\nconst cacheKey = async (key: any): Promise<string> => {\n    const keyObjString = JSON.stringify(_sortObjectDeep(key));\n    const keyMd5 = EncodeUtil.md5(keyObjString);\n    return path.join(await tempRoot(), `FileCache_${keyMd5}`);\n};\nconst cacheForget = async (key: any): Promise<void> => {\n    const keyPath = await cacheKey(key);\n    if (await exists(keyPath)) {\n        await deletes(keyPath);\n    }\n};\nconst cacheSet = async (key: any, data: any): Promise<void> => {\n    const keyPath = await cacheKey(key);\n    await write(keyPath, JSON.stringify(data));\n};\nconst cacheGet = async (key: any): Promise<any | null> => {\n    const keyPath = await cacheKey(key);\n    if (!(await exists(keyPath))) {\n        return null;\n    }\n    const content = await read(keyPath);\n    if (!content) {\n        return null;\n    }\n    try {\n        return JSON.parse(content);\n    } catch (e) {\n        return null;\n    }\n};\nconst cacheGetPath = async (key: any): Promise<string | null> => {\n    const p = await cacheGet(key);\n    if (!p) {\n        return null;\n    }\n    if (!(await exists(p))) {\n        await cacheForget(key);\n        return null;\n    }\n    return p;\n};\n\nexport const FileIndex = {\n    fullPath,\n    absolutePath,\n    exists,\n    isDirectory,\n    mkdir,\n    list,\n    listAll,\n    write,\n    writeStream,\n    writeBuffer,\n    read,\n    readBuffer,\n    readStream,\n    readLine,\n    clean,\n    deletes,\n    rename,\n    copy,\n    tempRoot,\n    tempName,\n    temp,\n    tempDir,\n    watchText,\n    appendText,\n    download,\n    ext,\n    stat,\n    textToName,\n    pathToName,\n    hubRootDefault,\n    hubRoot,\n    hubSave,\n    hubSaveContent,\n    hubDelete,\n    hubFile,\n    hubFullPath,\n    isHubFile,\n    cacheForget,\n    cacheSet,\n    cacheGetPath,\n    cacheGet,\n    autoCleanTemp,\n};\n\nexport default FileIndex;\n"
  },
  {
    "path": "electron/mapi/file/main.ts",
    "content": "import { dialog, ipcMain } from \"electron\";\nimport fileIndex from \"./index\";\n\nipcMain.handle(\n    \"file:openFile\",\n    async (\n        event,\n        options: {\n            filters?: {\n                name: string;\n                extensions: string[];\n            }[];\n            properties?: (\"multiSelections\" | \"openFile\")[];\n        } = {},\n    ): Promise<string | string[] | null> => {\n        options = Object.assign(\n            {\n                filters: [],\n                properties: [],\n            },\n            options,\n        );\n        if (!options.properties.includes(\"openFile\")) {\n            options.properties.push(\"openFile\");\n        }\n        // @ts-ignore\n        options.properties.push(\"noResolveAliases\");\n        const res = await dialog\n            .showOpenDialog({\n                ...options,\n            })\n            .catch((e) => {});\n        if (!res || res.canceled) {\n            return null;\n        }\n        if (options.properties.includes(\"multiSelections\")) {\n            return res.filePaths || null;\n        }\n        return res.filePaths?.[0] || null;\n    },\n);\n\nipcMain.handle(\n    \"file:openDirectory\",\n    async (_, options): Promise<string | null> => {\n        const res = await dialog\n            .showOpenDialog({\n                properties: [\"openDirectory\"],\n                ...options,\n            })\n            .catch((e) => {});\n        if (!res || res.canceled) {\n            return null;\n        }\n        return res.filePaths?.[0] || null;\n    },\n);\n\nipcMain.handle(\"file:openSave\", async (_, options): Promise<string | null> => {\n    const res = await dialog\n        .showSaveDialog({\n            ...options,\n        })\n        .catch((e) => {});\n    if (!res || res.canceled) {\n        return null;\n    }\n    return res.filePath || null;\n});\n\nconst autoCleanTemp = async () => {\n    fileIndex.autoCleanTemp(1).finally(() => {\n        setTimeout(\n            () => {\n                autoCleanTemp();\n            },\n            10 * 60 * 1000,\n        );\n    });\n};\n\nsetTimeout(() => {\n    autoCleanTemp().then();\n}, 5000);\n\nexport default {\n    ...fileIndex,\n};\n\nexport const Files = {\n    ...fileIndex,\n};\n"
  },
  {
    "path": "electron/mapi/file/render.ts",
    "content": "import fileIndex from \"./index\";\nimport { ipcRenderer } from \"electron\";\n\nconst openFile = async (options: {} = {}) => {\n    return ipcRenderer.invoke(\"file:openFile\", options);\n};\n\nconst openDirectory = async (options: {} = {}) => {\n    return ipcRenderer.invoke(\"file:openDirectory\", options);\n};\n\nconst openSave = async (options: {} = {}) => {\n    return ipcRenderer.invoke(\"file:openSave\", options);\n};\n\nexport default {\n    ...fileIndex,\n    openFile,\n    openDirectory,\n    openSave,\n};\n"
  },
  {
    "path": "electron/mapi/httpserver/main.ts",
    "content": "import type { Request, Response } from \"express\";\nimport express from \"express\";\nimport crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport http from \"node:http\";\nimport path from \"node:path\";\nimport { AppEnv } from \"../env\";\nimport { Log } from \"../log/main\";\nimport { Manager } from \"../manager/manager\";\n\nlet server: http.Server | null = null;\nlet isRunning = false;\nlet runningPort = 0;\nlet runningToken = \"\";\n\nconst getAvailablePort = (): Promise<number> => {\n    return new Promise((resolve, reject) => {\n        const s = http.createServer();\n        s.listen(0, \"127.0.0.1\", () => {\n            const addr = s.address() as { port: number };\n            const port = addr.port;\n            s.close(() => resolve(port));\n        });\n        s.on(\"error\", reject);\n    });\n};\n\nconst generateToken = (): string => {\n    return (\n        crypto.randomUUID().replace(/-/g, \"\") +\n        crypto.randomUUID().replace(/-/g, \"\")\n    );\n};\n\nconst writeCliAuthFile = (port: number, token: string): void => {\n    try {\n        const filePath = path.join(AppEnv.userData, \"cli-auth.json\");\n        fs.writeFileSync(filePath, JSON.stringify({ port, token }), \"utf-8\");\n    } catch (e) {\n        Log.error(\"httpserver.writeCliAuthFile.error\", e);\n    }\n};\n\nconst sendJson = (res: Response, statusCode: number, data: any) => {\n    res.status(statusCode).json(data);\n};\n\nconst createApp = (port: number, token: string) => {\n    const app = express();\n    app.use(express.json());\n    app.use((_req, res, next) => {\n        res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        res.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS\");\n        res.setHeader(\n            \"Access-Control-Allow-Headers\",\n            \"Content-Type, Authorization\",\n        );\n        if (_req.method === \"OPTIONS\") {\n            res.status(200).end();\n            return;\n        }\n        next();\n    });\n\n    app.use((_req, res, next) => {\n        const auth = _req.headers[\"authorization\"] || \"\";\n        if (!auth.startsWith(\"Bearer \") || auth.slice(7) !== token) {\n            sendJson(res, 401, { code: -1, msg: \"Unauthorized\" });\n            return;\n        }\n        next();\n    });\n\n    app.get(\"/api/plugin/list\", async (_req: Request, res: Response) => {\n        try {\n            const plugins = await Manager.listPlugin();\n            const list = plugins.map((p) => ({\n                name: p.name,\n                title: p.title,\n                version: p.version,\n                logo: p.logo,\n                type: p.type,\n                description: p.description || \"\",\n            }));\n            sendJson(res, 200, { code: 0, data: { list } });\n        } catch (e) {\n            sendJson(res, 500, { code: -1, msg: String(e) });\n        }\n    });\n\n    return app;\n};\n\nexport const HttpServer = {\n    async start() {\n        if (isRunning) {\n            return;\n        }\n        try {\n            const port = await getAvailablePort();\n            const token = generateToken();\n            const app = createApp(port, token);\n            server = http.createServer(app);\n            await new Promise<void>((resolve, reject) => {\n                server!.listen(port, \"127.0.0.1\", () => resolve());\n                server!.on(\"error\", reject);\n            });\n            runningPort = port;\n            runningToken = token;\n            isRunning = true;\n            writeCliAuthFile(port, token);\n            Log.info(\"httpserver.start\", { port });\n        } catch (e) {\n            Log.error(\"httpserver.start.error\", e);\n        }\n    },\n\n    stop() {\n        if (server) {\n            server.close();\n            server = null;\n            isRunning = false;\n            runningPort = 0;\n            runningToken = \"\";\n        }\n    },\n\n    getPort() {\n        return runningPort;\n    },\n\n    getToken() {\n        return runningToken;\n    },\n};\n"
  },
  {
    "path": "electron/mapi/keys/main.ts",
    "content": "import { app, BrowserWindow, globalShortcut } from \"electron\";\nimport { AppsMain } from \"../app/main\";\nimport { ManagerHotkey } from \"../manager/hotkey\";\n\nconst eventListeners = {};\n\n// 连续点击的快捷键\nlet continuousKeys = [];\nconst addKeyInput = (key: string, expire = 1000) => {\n    let now = Date.now();\n    continuousKeys.push({ key, expire: now + expire });\n    continuousKeys = continuousKeys.filter((item) => item.expire > now);\n    for (let i = continuousKeys.length - 1; i >= 0; i--) {\n        const key = continuousKeys\n            .filter((o, oIndex) => oIndex >= i)\n            .map((o) => o.key)\n            .join(\"|\");\n        if (eventListeners[key]) {\n            eventListeners[key]();\n            break;\n        }\n    }\n};\n\nconst addMultiKeyListener = (keys: string[], callback: Function) => {\n    if (!Array.isArray(keys)) {\n        keys = [keys];\n    }\n    const key = keys.join(\"|\");\n    eventListeners[key] = callback;\n};\n\nconst createKeyInputListener = (key: string) => {\n    return () => {\n        addKeyInput(key);\n    };\n};\n\nconst keyMap = {\n    \"CommandOrControl+Shift+H\": createKeyInputListener(\n        \"CommandOrControl+Shift+H\",\n    ),\n};\n\nconst ready = () => {\n    register();\n};\n\nconst destroy = () => {\n    globalShortcut.unregisterAll();\n};\n\nconst register = () => {\n    globalShortcut.unregisterAll();\n\n    app.on(\"browser-window-focus\", () => {\n        for (let key in keyMap) {\n            globalShortcut.register(key, keyMap[key]);\n        }\n    });\n\n    app.on(\"browser-window-blur\", () => {\n        for (let key in keyMap) {\n            globalShortcut.unregister(key);\n        }\n    });\n\n    addMultiKeyListener(\n        [\n            \"CommandOrControl+Shift+H\",\n            \"CommandOrControl+Shift+H\",\n            \"CommandOrControl+Shift+H\",\n        ],\n        () => {\n            let focusedWindow = BrowserWindow.getFocusedWindow();\n            if (focusedWindow) {\n                if (focusedWindow.webContents.isDevToolsOpened()) {\n                    focusedWindow.webContents.closeDevTools();\n                } else {\n                    focusedWindow.webContents.openDevTools({\n                        mode: \"detach\",\n                        activate: false,\n                        title: \"FocusedWindow\",\n                    });\n                }\n            }\n        },\n    );\n\n    ManagerHotkey.register().then();\n};\n\nexport const KeysMain = {\n    register,\n};\n\nexport default {\n    ready,\n    destroy,\n};\n"
  },
  {
    "path": "electron/mapi/keys/type.ts",
    "content": "export enum HotkeyMouseButtonEnum {\n    LEFT = 1,\n    RIGHT = 2,\n}\n\nexport type HotkeyKeyItem = {\n    key: string;\n    // Alt Option\n    altKey: boolean;\n    // Ctrl Control\n    ctrlKey: boolean;\n    // Command Win\n    metaKey: boolean;\n    // Shift\n    shiftKey: boolean;\n    times: number;\n};\n\nexport type HotkeyKeySimpleItem = {\n    type: \"Ctrl\" | \"Alt\" | \"Meta\";\n    times: number;\n};\n\nexport type HotkeyMouseItem = {\n    button: HotkeyMouseButtonEnum;\n    type: \"click\" | \"longPress\";\n    clickTimes?: number;\n};\n"
  },
  {
    "path": "electron/mapi/kvdb/kvdb.ts",
    "content": "import path from \"path\";\nimport fs from \"fs\";\nimport PouchDB from \"pouchdb\";\nimport { DBError, Doc, DocRes } from \"./types\";\n\nimport replicationStream from \"pouchdb-replication-stream\";\nimport load from \"pouchdb-load\";\nimport { KVDBVersionManager } from \"./version\";\nimport { Log } from \"../log/main\";\n\nimport ndj from \"ndjson\";\nimport through from \"through2\";\nimport { WebDav } from \"./webdav\";\nimport { AppEnv } from \"../env\";\n\nPouchDB.plugin(replicationStream.plugin);\n// @ts-ignore\nPouchDB.adapter(\"writableStream\", replicationStream.adapters.writableStream);\nPouchDB.plugin({ loadIt: load.load });\n\nexport default class KVDB {\n    readonly docMaxByteLength;\n    readonly docAttachmentMaxByteLength;\n    public dbpath;\n    public defaultDbName;\n    public pouchDB: any;\n    public versionControl: boolean;\n\n    constructor() {\n        // 2M\n        this.docMaxByteLength = 2 * 1024 * 1024;\n        // 20M\n        this.docAttachmentMaxByteLength = 20 * 1024 * 1024;\n        let dbPath = AppEnv.dataRoot;\n        if (fs.existsSync(path.join(AppEnv.userData, \"kvdb\"))) {\n            dbPath = AppEnv.userData;\n        }\n        this.dbpath = dbPath;\n        this.defaultDbName = path.join(dbPath, \"kvdb\");\n        this.versionControl = true;\n    }\n\n    init(): void {\n        fs.existsSync(this.dbpath) ||\n            fs.mkdirSync(this.dbpath, {\n                recursive: true,\n            });\n        this.pouchDB = new PouchDB(this.defaultDbName, {\n            auto_compaction: true,\n            adapter: \"leveldb\",\n        });\n    }\n\n    getDocId(name: string, id: string): string {\n        return name + \"/\" + id;\n    }\n\n    replaceDocId(name: string, id: string): string {\n        return id.replace(name + \"/\", \"\");\n    }\n\n    errorInfo(name: string, message: string): DBError {\n        return { error: true, name, message };\n    }\n\n    private checkDocSize(doc: Doc) {\n        if (Buffer.byteLength(JSON.stringify(doc)) > this.docMaxByteLength) {\n            return this.errorInfo(\n                \"exception\",\n                `doc max size ${this.docMaxByteLength / 1024 / 1024} M`,\n            );\n        }\n        return false;\n    }\n\n    async put(\n        name: string,\n        doc: Doc,\n        strict = true,\n    ): Promise<DBError | DocRes> {\n        if (strict) {\n            const err = this.checkDocSize(doc);\n            if (err) return err;\n        }\n        doc._id = this.getDocId(name, doc._id);\n        try {\n            const result: DocRes = await this.pouchDB.put(doc);\n            if (this.versionControl) {\n                if (doc._rev) {\n                    KVDBVersionManager.update(doc._id).then();\n                } else {\n                    KVDBVersionManager.insert(doc._id).then();\n                }\n            }\n            doc._id = result.id = this.replaceDocId(name, result.id);\n            return result;\n        } catch (e: any) {\n            doc._id = this.replaceDocId(name, doc._id);\n            return { id: doc._id, name: e.name, error: !0, message: e.message };\n        }\n    }\n\n    async putRaw(doc: Doc): Promise<DBError | DocRes> {\n        let result: Doc | null = null;\n        try {\n            result = await this.pouchDB.get(doc._id);\n        } catch (e) {}\n        if (result) {\n            doc._rev = result._rev;\n        }\n        try {\n            return await this.pouchDB.put(doc);\n        } catch (e: any) {\n            return { id: doc._id, name: e.name, error: !0, message: e.message };\n        }\n    }\n\n    async get(name: string, id: string): Promise<Doc | null> {\n        try {\n            const result: Doc = await this.pouchDB.get(this.getDocId(name, id));\n            result._id = this.replaceDocId(name, result._id);\n            return result;\n        } catch (e) {\n            return null;\n        }\n    }\n\n    async getRaw(id: string) {\n        try {\n            return await this.pouchDB.get(id);\n        } catch (e) {\n            return null;\n        }\n    }\n\n    async remove(name: string, doc: Doc | string) {\n        try {\n            let target;\n            if (\"object\" == typeof doc) {\n                target = doc;\n                if (!target._id || \"string\" !== typeof target._id) {\n                    return this.errorInfo(\"exception\", \"doc _id error\");\n                }\n                target._id = this.getDocId(name, target._id);\n            } else {\n                if (\"string\" !== typeof doc) {\n                    return this.errorInfo(\"exception\", \"param error\");\n                }\n                target = await this.pouchDB.get(this.getDocId(name, doc));\n            }\n            const result: DocRes = await this.pouchDB.remove(target);\n            if (this.versionControl) {\n                KVDBVersionManager.remove(target._id).then();\n            }\n            target._id = result.id = this.replaceDocId(name, result.id);\n            return result;\n        } catch (e: any) {\n            if (\"object\" === typeof doc) {\n                doc._id = this.replaceDocId(name, doc._id);\n            }\n            return this.errorInfo(e.name, e.message);\n        }\n    }\n\n    async removeRaw(doc: Doc) {\n        try {\n            return await this.pouchDB.remove(doc);\n        } catch (e) {\n            return null;\n        }\n    }\n\n    async bulkPut(\n        name: string,\n        docs: Array<Doc<any>>,\n    ): Promise<DBError | Array<DocRes>> {\n        let result;\n        try {\n            if (!Array.isArray(docs))\n                return this.errorInfo(\"exception\", \"not array\");\n            if (docs.find((e) => !e._id))\n                return this.errorInfo(\"exception\", \"doc not _id field\");\n            if (new Set(docs.map((e) => e._id)).size !== docs.length)\n                return this.errorInfo(\"exception\", \"_id value exists as\");\n            for (const doc of docs) {\n                const err = this.checkDocSize(doc);\n                if (err) return err;\n                doc._id = this.getDocId(name, doc._id);\n            }\n            result = await this.pouchDB.bulkDocs(docs);\n            result = result.map((res: any) => {\n                res.id = this.replaceDocId(name, res.id);\n                return res.error\n                    ? {\n                          id: res.id,\n                          name: res.name,\n                          error: true,\n                          message: res.message,\n                      }\n                    : res;\n            });\n            docs.forEach((doc) => {\n                if (this.versionControl) {\n                    if (doc._rev) {\n                        KVDBVersionManager.update(doc._id).then();\n                    } else {\n                        KVDBVersionManager.insert(doc._id).then();\n                    }\n                }\n                doc._id = this.replaceDocId(name, doc._id);\n            });\n        } catch (e) {\n            //\n        }\n        return result;\n    }\n\n    async all(\n        name: string,\n        key: string | Array<string>,\n    ): Promise<DBError | Array<Doc<any>>> {\n        const config: any = { include_docs: true };\n        if (key) {\n            if (\"string\" == typeof key) {\n                config.startkey = this.getDocId(name, key);\n                config.endkey = config.startkey + \"￰\";\n            } else {\n                if (!Array.isArray(key))\n                    return this.errorInfo(\n                        \"exception\",\n                        \"param only key(string) or keys(Array[string])\",\n                    );\n                config.keys = key.map((key) => this.getDocId(name, key));\n            }\n        } else {\n            config.startkey = this.getDocId(name, \"\");\n            config.endkey = config.startkey + \"￰\";\n        }\n        const result: Array<any> = [];\n        try {\n            (await this.pouchDB.allDocs(config)).rows.forEach((res: any) => {\n                if (!res.error && res.doc) {\n                    res.doc._id = this.replaceDocId(name, res.doc._id);\n                    result.push(res.doc);\n                }\n            });\n        } catch (e) {\n            //\n        }\n        return result;\n    }\n\n    async allKeys(\n        name: string,\n        key: string | Array<string>,\n    ): Promise<DBError | Array<string>> {\n        const config: any = { include_docs: false };\n        if (key) {\n            if (\"string\" == typeof key) {\n                config.startkey = this.getDocId(name, key);\n                config.endkey = config.startkey + \"￰\";\n            } else {\n                if (!Array.isArray(key))\n                    return this.errorInfo(\n                        \"exception\",\n                        \"param only key(string) or keys(Array[string])\",\n                    );\n                config.keys = key.map((key) => this.getDocId(name, key));\n            }\n        } else {\n            config.startkey = this.getDocId(name, \"\");\n            config.endkey = config.startkey + \"￰\";\n        }\n        const result: Array<any> = [];\n        try {\n            (await this.pouchDB.allDocs(config)).rows.forEach((res: any) => {\n                if (!res.error && res.id) {\n                    const id = this.replaceDocId(name, res.id);\n                    result.push(id);\n                }\n            });\n        } catch (e) {\n            //\n        }\n        return result;\n    }\n\n    async count(\n        name: string,\n        key: string | Array<string>,\n    ): Promise<DBError | number> {\n        const config: any = { include_docs: false };\n        if (key) {\n            if (\"string\" == typeof key) {\n                config.startkey = this.getDocId(name, key);\n                config.endkey = config.startkey + \"￰\";\n            } else {\n                if (!Array.isArray(key))\n                    return this.errorInfo(\n                        \"exception\",\n                        \"param only key(string) or keys(Array[string])\",\n                    );\n                config.keys = key.map((key) => this.getDocId(name, key));\n            }\n        } else {\n            config.startkey = this.getDocId(name, \"\");\n            config.endkey = config.startkey + \"￰\";\n        }\n        try {\n            return (await this.pouchDB.allDocs(config)).rows.length;\n        } catch (e) {\n            //\n        }\n        return 0;\n    }\n\n    public async postAttachment(\n        name: string,\n        docId: string,\n        attachment: Buffer | Uint8Array,\n        type: string,\n    ) {\n        const buffer = Buffer.from(attachment);\n        if (buffer.byteLength > this.docAttachmentMaxByteLength)\n            return this.errorInfo(\n                \"exception\",\n                \"attachment data up to \" +\n                    this.docAttachmentMaxByteLength / 1024 / 1024 +\n                    \"M\",\n            );\n        try {\n            const result = await this.pouchDB.put({\n                _id: this.getDocId(name, docId),\n                _attachments: { 0: { data: buffer, content_type: type } },\n            });\n            if (this.versionControl) {\n                KVDBVersionManager.insert(result.id).then();\n            }\n            result.id = this.replaceDocId(name, result.id);\n            return result;\n        } catch (e) {\n            return this.errorInfo(e.name, e.message);\n        }\n    }\n\n    async getAttachment(name: string, docId: string, len = \"0\") {\n        try {\n            return await this.pouchDB.getAttachment(\n                this.getDocId(name, docId),\n                len,\n            );\n        } catch (e) {\n            return null;\n        }\n    }\n\n    async getAttachmentRaw(docId: string, len = \"0\") {\n        try {\n            return await this.pouchDB.getAttachment(docId, len);\n        } catch (e) {\n            return null;\n        }\n    }\n\n    public async dumpToFile(file: string, option?: {}): Promise<void> {\n        try {\n            const writeStream = fs.createWriteStream(file);\n            await this.pouchDB.dump(writeStream, {\n                batch_size: 10,\n            });\n        } catch (e) {\n            Log.info(\"kvdb.dumpToFile.error\", e);\n            throw e;\n        }\n    }\n\n    public async importFromFile(file: string, option?: {}): Promise<void> {\n        await this.pouchDB.destroy();\n        const syncDb = new KVDB();\n        syncDb.init();\n        this.pouchDB = syncDb.pouchDB;\n        const rs = fs.createReadStream(file);\n        try {\n            await this.load(rs);\n        } catch (e) {\n            Log.info(\"kvdb.importFromFile.error\", e);\n            throw e;\n        }\n    }\n\n    public async dumpToWavDav(\n        file: string,\n        option: {\n            url: string;\n            username: string;\n            password: string;\n        },\n    ): Promise<void> {\n        try {\n            const webdav = new WebDav(option);\n            await webdav.dump(this, file);\n        } catch (e) {\n            Log.info(\"kvdb.dumpToWavDav.error\", e);\n            throw e;\n        }\n    }\n\n    public async importFromWebDav(\n        file: string,\n        option: {\n            url: string;\n            username: string;\n            password: string;\n        },\n    ): Promise<void> {\n        await this.pouchDB.destroy();\n        const syncDb = new KVDB();\n        syncDb.init();\n        this.pouchDB = syncDb.pouchDB;\n        try {\n            const webdav = new WebDav(option);\n            await webdav.import(this, file);\n        } catch (e) {\n            Log.info(\"kvdb.importFromWebDav.error\", e);\n            throw e;\n        }\n    }\n\n    public async load(readableStream: any) {\n        return new Promise((resolve, reject) => {\n            let error = null;\n            let queue = [];\n            readableStream\n                .pipe(ndj.parse())\n                .on(\"error\", function (errorCatched) {\n                    error = errorCatched;\n                })\n                .pipe(\n                    through.obj(function (data, _, next) {\n                        if (!data.docs) {\n                            return next();\n                        }\n                        // lets smooth it out\n                        data.docs.forEach(function (doc) {\n                            this.push(doc);\n                        }, this);\n                        next();\n                    }),\n                )\n                .pipe(\n                    through.obj(\n                        function (doc, _, next) {\n                            // console.log('doc', doc)\n                            if (doc._attachments) {\n                                for (const k in doc._attachments) {\n                                    if (doc._attachments[k].data) {\n                                        // console.log('doc._attachments[k].data', k, doc._attachments[k].data)\n                                        const bytes =\n                                            doc._attachments[k].data.data;\n                                        const base64 = new Buffer(\n                                            bytes,\n                                        ).toString(\"base64\");\n                                        doc._attachments[k].data = base64;\n                                    }\n                                }\n                            }\n                            queue.push(doc);\n                            if (queue.length >= 10) {\n                                this.push(queue);\n                                queue = [];\n                            }\n                            next();\n                        },\n                        function (next) {\n                            if (queue.length) {\n                                this.push(queue);\n                            }\n                            next();\n                        },\n                    ),\n                )\n                .pipe(this.pouchDB.createWriteStream({ new_edits: false }))\n                .on(\"error\", function (errorCatched) {\n                    error = errorCatched;\n                })\n                .on(\"finish\", function () {\n                    if (error) {\n                        reject(error);\n                    } else {\n                        resolve(undefined);\n                    }\n                });\n        });\n    }\n}\n"
  },
  {
    "path": "electron/mapi/kvdb/main.ts",
    "content": "import KVDB from \"./kvdb\";\nimport { AppEnv } from \"../env\";\nimport { DBError, Doc } from \"./types\";\nimport { ipcMain } from \"electron\";\nimport { WebDav } from \"./webdav\";\n\nlet kvdb: KVDB = null;\n\nconst init = () => {\n    kvdb = new KVDB();\n    kvdb.init();\n    // for (let i = 0; i < 1000; i++) {\n    //     kvdb.putRaw({\n    //         _id: `data${i}`,\n    //         data: i\n    //     })\n    // }\n    // const sync = async () => {\n    //     KVDBCloudManager.sync().then(() => {\n    //         setTimeout(sync, 5000)\n    //     })\n    // }\n    // setTimeout(sync, 1000)\n};\n\nconst raw = () => {\n    return kvdb;\n};\n\nconst put = async (name: string, data: Doc<any>) => {\n    const result = await kvdb.put(name, data);\n    if (result && (result as DBError).error) {\n        throw (result as DBError).message;\n    }\n    return result as Doc<any>;\n};\n\nconst putForceLock = new Map<string, Promise<any>>();\nconst putForce = async (name: string, data: Doc<any>) => {\n    while (putForceLock.has(name)) {\n        await putForceLock.get(name);\n    }\n    let release!: () => void;\n    const currentTask = new Promise<void>((resolve) => {\n        release = resolve;\n    });\n    putForceLock.set(name, currentTask);\n    try {\n        const res = await get(name, data._id);\n        if (res) {\n            data._rev = res._rev;\n        }\n        const result = await put(name, data);\n        if (result && (result as DBError).error) {\n            throw (result as DBError).message;\n        }\n        return result as Doc<any>;\n    } finally {\n        putForceLock.delete(name);\n        release();\n    }\n};\n\nconst get = async (name: string, id: string) => {\n    return await kvdb.get(name, id);\n};\n\nconst getData = async (name: string, id: string, defaultValue: any = null) => {\n    const res = await get(name, id);\n    if (res) {\n        delete res._id;\n        delete res._rev;\n        delete res._attachments;\n    }\n    return res ? res : defaultValue;\n};\n\nconst remove = async (name: string, doc: Doc<any> | string) => {\n    return await kvdb.remove(name, doc);\n};\n\nconst bulkDocs = async (name: string, docs: any[]) => {\n    const result = await kvdb.bulkPut(name, docs);\n    if (result && (result as DBError).error) {\n        throw (result as DBError).message;\n    }\n    return result as Doc<any>[];\n};\n\nconst allDocs = async (name: string, key: string): Promise<Doc[]> => {\n    const result = await kvdb.all(name, key);\n    if (result && (result as DBError).error) {\n        throw (result as DBError).message;\n    }\n    return result as Doc<any>[];\n};\n\nconst allKeys = async (name: string, key: string): Promise<string[]> => {\n    const result = await kvdb.allKeys(name, key);\n    if (result && (result as DBError).error) {\n        throw (result as DBError).message;\n    }\n    return result as string[];\n};\n\nconst count = async (name: string, key: string) => {\n    const result = await kvdb.count(name, key);\n    if (result && (result as DBError).error) {\n        throw (result as DBError).message;\n    }\n    return result as number;\n};\n\nconst postAttachment = async (\n    name: string,\n    docId: string,\n    attachment: any,\n    type: string,\n) => {\n    return await kvdb.postAttachment(name, docId, attachment, type);\n};\n\nconst getAttachment = async (name: string, docId: string) => {\n    return await kvdb.getAttachment(name, docId);\n};\n\nconst getAttachmentType = async (name: string, docId: string) => {\n    const res = await get(name, docId);\n    if (!res || !res._attachments) return null;\n    const result = res._attachments[0];\n    return result ? result.content_type : null;\n};\n\nconst dumpToFile = async (file: string) => {\n    return await kvdb.dumpToFile(file);\n};\n\nconst importFromFile = async (file: string) => {\n    return await kvdb.importFromFile(file);\n};\n\nconst testWebdav = async (option: {\n    url: string;\n    username: string;\n    password: string;\n}) => {\n    const webdav = new WebDav(option);\n    await webdav.checkConnection();\n};\n\nconst dumpToWebDav = async (\n    file: string,\n    option: {\n        url: string;\n        username: string;\n        password: string;\n    },\n) => {\n    return await kvdb.dumpToWavDav(file, option);\n};\n\nconst importFromWebDav = async (\n    file: string,\n    option: {\n        url: string;\n        username: string;\n        password: string;\n    },\n) => {\n    return await kvdb.importFromWebDav(file, option);\n};\n\nconst listWebDav = async (\n    dir: string,\n    option: {\n        url: string;\n        username: string;\n        password: string;\n    },\n) => {\n    const webdav = new WebDav(option);\n    await webdav.checkConnection();\n    return await webdav.listDir(dir);\n};\n\nipcMain.handle(\"kvdb:put\", (event, name: string, data: Doc<any>) => {\n    return put(name, data);\n});\n\nipcMain.handle(\"kvdb:putForce\", (event, name: string, data: Doc<any>) => {\n    return putForce(name, data);\n});\n\nipcMain.handle(\"kvdb:get\", (event, name: string, id: string) => {\n    return get(name, id);\n});\n\nipcMain.handle(\"kvdb:remove\", (event, name: string, doc: Doc<any> | string) => {\n    return remove(name, doc);\n});\n\nipcMain.handle(\"kvdb:bulkDocs\", (event, name: string, docs: any[]) => {\n    return bulkDocs(name, docs);\n});\n\nipcMain.handle(\"kvdb:allDocs\", (event, name: string, key: string) => {\n    return allDocs(name, key);\n});\n\nipcMain.handle(\"kvdb:allKeys\", (event, name: string, key: string) => {\n    return allKeys(name, key);\n});\n\nipcMain.handle(\"kvdb:count\", (event, name: string, key: string) => {\n    return count(name, key);\n});\n\nipcMain.handle(\n    \"kvdb:postAttachment\",\n    (event, name: string, docId: string, attachment: any, type: string) => {\n        return postAttachment(name, docId, attachment, type);\n    },\n);\n\nipcMain.handle(\"kvdb:getAttachment\", (event, name: string, docId: string) => {\n    return getAttachment(name, docId);\n});\n\nipcMain.handle(\n    \"kvdb:getAttachmentType\",\n    (event, name: string, docId: string) => {\n        return getAttachmentType(name, docId);\n    },\n);\n\nipcMain.handle(\"kvdb:dumpToFile\", (event, file: string) => {\n    return dumpToFile(file);\n});\n\nipcMain.handle(\"kvdb:importFromFile\", (event, file: string) => {\n    return importFromFile(file);\n});\n\nipcMain.handle(\n    \"kvdb:testWebdav\",\n    (\n        event,\n        option: {\n            url: string;\n            username: string;\n            password: string;\n        },\n    ) => {\n        return testWebdav(option);\n    },\n);\n\nipcMain.handle(\n    \"kvdb:dumpToWebDav\",\n    (\n        event,\n        file: string,\n        option: {\n            url: string;\n            username: string;\n            password: string;\n        },\n    ) => {\n        return dumpToWebDav(file, option);\n    },\n);\n\nipcMain.handle(\n    \"kvdb:importFromWebDav\",\n    (\n        event,\n        file: string,\n        option: {\n            url: string;\n            username: string;\n            password: string;\n        },\n    ) => {\n        return importFromWebDav(file, option);\n    },\n);\n\nipcMain.handle(\n    \"kvdb:listWebDav\",\n    (\n        event,\n        dir: string,\n        option: {\n            url: string;\n            username: string;\n            password: string;\n        },\n    ) => {\n        return listWebDav(dir, option);\n    },\n);\n\nexport const KVDBMain = {\n    raw,\n    put,\n    putForce,\n    get,\n    getData,\n    remove,\n    bulkDocs,\n    allDocs,\n    allKeys,\n    postAttachment,\n    getAttachment,\n    getAttachmentType,\n    dumpToFile,\n    importFromFile,\n    dumpToWebDav,\n    importFromWebDav,\n    listWebDav,\n};\n\nexport default {\n    init,\n    ...KVDBMain,\n};\n"
  },
  {
    "path": "electron/mapi/kvdb/render.ts",
    "content": "import { Doc } from \"./types\";\nimport { ipcRenderer } from \"electron\";\n\nconst put = async (name: string, doc: Doc) => {\n    return ipcRenderer.invoke(\"kvdb:put\", name, doc);\n};\n\nconst putForce = async (name: string, doc: Doc) => {\n    return ipcRenderer.invoke(\"kvdb:putForce\", name, doc);\n};\n\nconst get = async (name: string, id: string) => {\n    return ipcRenderer.invoke(\"kvdb:get\", name, id);\n};\n\nconst remove = async (name: string, doc: Doc | string) => {\n    return ipcRenderer.invoke(\"kvdb:remove\", name, doc);\n};\n\nconst bulkDocs = async (name: string, docs: any[]) => {\n    return ipcRenderer.invoke(\"kvdb:bulkDocs\", name, docs);\n};\n\nconst allDocs = async (name: string, key: string) => {\n    return ipcRenderer.invoke(\"kvdb:allDocs\", name, key);\n};\n\nconst allKeys = async (name: string, key: string) => {\n    return ipcRenderer.invoke(\"kvdb:allKeys\", name, key);\n};\n\nconst count = async (name: string, key: string) => {\n    return ipcRenderer.invoke(\"kvdb:count\", name, key);\n};\n\nconst postAttachment = async (\n    name: string,\n    docId: string,\n    attachment: any,\n    type: string,\n) => {\n    return ipcRenderer.invoke(\n        \"kvdb:postAttachment\",\n        name,\n        docId,\n        attachment,\n        type,\n    );\n};\n\nconst getAttachment = async (name: string, docId: string) => {\n    return ipcRenderer.invoke(\"kvdb:getAttachment\", name, docId);\n};\n\nconst getAttachmentType = async (name: string, docId: string) => {\n    return ipcRenderer.invoke(\"kvdb:getAttachmentType\", name, docId);\n};\n\nconst dumpToFile = async (file: string) => {\n    return ipcRenderer.invoke(\"kvdb:dumpToFile\", file);\n};\n\nconst importFromFile = async (file: string) => {\n    return ipcRenderer.invoke(\"kvdb:importFromFile\", file);\n};\n\nconst testWebdav = async (option: {\n    url: string;\n    username: string;\n    password: string;\n}) => {\n    return ipcRenderer.invoke(\"kvdb:testWebdav\", option);\n};\n\nconst dumpToWebDav = async (\n    file: string,\n    option: {\n        url: string;\n        username: string;\n        password: string;\n    },\n) => {\n    return ipcRenderer.invoke(\"kvdb:dumpToWebDav\", file, option);\n};\n\nconst importFromWebDav = async (\n    file: string,\n    option: {\n        url: string;\n        username: string;\n        password: string;\n    },\n) => {\n    return ipcRenderer.invoke(\"kvdb:importFromWebDav\", file, option);\n};\n\nconst listWebDav = async (\n    dir: string,\n    option: {\n        url: string;\n        username: string;\n        password: string;\n    },\n) => {\n    return ipcRenderer.invoke(\"kvdb:listWebDav\", dir, option);\n};\n\nexport default {\n    put,\n    putForce,\n    get,\n    remove,\n    bulkDocs,\n    allDocs,\n    allKeys,\n    count,\n    postAttachment,\n    getAttachment,\n    getAttachmentType,\n    dumpToFile,\n    importFromFile,\n    testWebdav,\n    dumpToWebDav,\n    importFromWebDav,\n    listWebDav,\n};\n"
  },
  {
    "path": "electron/mapi/kvdb/types.ts",
    "content": "type RevisionId = string;\n\nexport type Doc<T extends {} = Record<string, any>> = {\n    _id: string;\n    _rev?: string;\n    _attachments?: any;\n} & T;\n\nexport interface DocRes {\n    id: string;\n    ok: boolean;\n    rev: RevisionId;\n    _id: string;\n    data?: any;\n}\n\nexport interface DBError {\n    status?: number | undefined;\n    name?: string | undefined;\n    message?: string | undefined;\n    reason?: string | undefined;\n    error?: string | boolean | undefined;\n    id?: string | undefined;\n    rev?: RevisionId | undefined;\n}\n\nexport interface AllDocsOptions {\n    include_docs?: boolean;\n    startkey?: string;\n    endkey?: string;\n    keys?: string[];\n}\n"
  },
  {
    "path": "electron/mapi/kvdb/version.ts",
    "content": "import DBMain from \"../db/main\";\nimport { StrUtil } from \"../../lib/util\";\n\nexport const KVDBVersionManager = {\n    async _getExist(name: string) {\n        const records = await DBMain.select(\n            \"select * from kvdb_data where name = ? and isDeleted = 0\",\n            [name],\n        );\n        for (let i = 1; i < records.length; i++) {\n            await DBMain.delete(\"delete from kvdb_data where id = ?\", [\n                records[i].id,\n            ]);\n        }\n        return records.length > 0 ? records[0] : null;\n    },\n    async update(name: string) {\n        if (this.shouldIgnore(name)) {\n            return;\n        }\n        // console.log('update', {name})\n        const exist = await this._getExist(name);\n        if (exist) {\n            await DBMain.update(\n                \"update kvdb_data set version = -1 where id = ?\",\n                [exist.id],\n            );\n        } else {\n            await DBMain.insert(\n                \"insert into kvdb_data (id, name, version, cloudVersion, isDeleted) values (?,?,-1,0,0)\",\n                [StrUtil.bigIntegerId(), name],\n            );\n        }\n    },\n    async insert(name: string) {\n        await this.update(name);\n    },\n    async remove(name: string) {\n        if (this.shouldIgnore(name)) {\n            return;\n        }\n        const exist = await this._getExist(name);\n        // console.log('remove', {name, exist})\n        if (exist) {\n            await DBMain.update(\n                \"update kvdb_data set isDeleted = 1, version = -1 where id = ?\",\n                [exist.id],\n            );\n        } else {\n            await DBMain.insert(\n                \"insert into kvdb_data (id, name, version, cloudVersion, isDeleted ) values (?, ?, -1, 0, 1)\",\n                [StrUtil.bigIntegerId(), name],\n            );\n        }\n    },\n    shouldIgnore(name: string) {\n        return [\n            // 系统存储版本号的kvdb\n            \"SYS/syncCloudVersion\",\n        ].includes(name);\n    },\n};\n"
  },
  {
    "path": "electron/mapi/kvdb/webdav.ts",
    "content": "import MemoryStream from \"memorystream\";\n\nimport { AuthType, createClient } from \"webdav\";\nimport { WebDAVClient } from \"webdav/dist/node/types\";\nimport KVDB from \"./kvdb\";\n\ntype WebDavOptions = {\n    username: string;\n    password: string;\n    url: string;\n};\n\nexport class WebDav {\n    public client: WebDAVClient;\n\n    constructor({ username, password, url }: WebDavOptions) {\n        // console.log('WebDavOptions', {username, password, url})\n        this.client = createClient(url, {\n            authType: AuthType.Auto,\n            username,\n            password,\n        });\n    }\n\n    async checkConnection(): Promise<void> {\n        await this.client.exists(\"/\");\n    }\n\n    async listDir(dir: string): Promise<string[]> {\n        dir = dir.endsWith(\"/\") ? dir : dir + \"/\";\n        const result = await this.client.getDirectoryContents(dir);\n        return (result as any[]).map((item) => item.basename);\n    }\n\n    async dump(kvdb: KVDB, file: string): Promise<void> {\n        await this.checkConnection();\n        const fileDir = file.substring(0, file.lastIndexOf(\"/\"));\n        if (!(await this.client.exists(fileDir + \"/\"))) {\n            await this.client.createDirectory(fileDir, {\n                recursive: true,\n            });\n        }\n        const ws = new MemoryStream();\n        kvdb.pouchDB.dump(ws, {\n            batch_size: 10,\n        });\n        return new Promise((resolve, reject) => {\n            ws.pipe(\n                this.client.createWriteStream(file, {}, () => {\n                    resolve();\n                }),\n            );\n        });\n    }\n\n    async import(kvdb: KVDB, file: string): Promise<void> {\n        // console.log('import', file)\n        await this.checkConnection();\n        if (!(await this.client.exists(file))) {\n            throw \"FileNotFound\";\n        }\n        const rs = this.client.createReadStream(file);\n        await kvdb.load(rs);\n    }\n}\n"
  },
  {
    "path": "electron/mapi/log/beacon-render.ts",
    "content": "/**\n * 渲染进程异常上报（HTTP Beacon）\n * 仅在 isPackaged（非开发）模式下上报，批量异步发送。\n */\nimport { AppConfig } from \"../../../src/config\";\n\ndeclare const __BUILD_ID__: string;\n\nconst BEACON_URL = \"https://g.tecmz.com/grow/load.gif\";\nconst BEACON_APP = \"focusany\";\nconst isPackaged = process.env[\"IS_PACKAGED\"] === \"true\";\nconst sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\nconst buildId = typeof __BUILD_ID__ !== \"undefined\" ? __BUILD_ID__ : \"unknown\";\n\ninterface BeaconEvent {\n    et: \"error\";\n    path: string;\n    did: string;\n    sid: string;\n    ts: number;\n    type: string;\n    bid: string;\n    props: {\n        msg: string;\n        stack?: string;\n        src?: string;\n        line?: number;\n        col?: number;\n    };\n}\n\nlet pending: BeaconEvent[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\nconst flush = () => {\n    if (!pending.length) return;\n    const events = pending.splice(0);\n    try {\n        const encoded = encodeURIComponent(btoa(JSON.stringify(events)));\n        const url = `${BEACON_URL}?app=${BEACON_APP}&data=${encoded}`;\n        fetch(url).catch(() => {});\n    } catch {}\n};\n\nconst schedule = () => {\n    if (timer) return;\n    timer = setTimeout(() => {\n        timer = null;\n        flush();\n    }, 3000);\n};\n\nexport const reportErrorRender = (\n    msg: string,\n    stack?: string,\n    src?: string,\n    line?: number,\n    col?: number,\n    path = \"/renderer\",\n) => {\n    if (!isPackaged) return;\n    pending.push({\n        et: \"error\",\n        path,\n        did: \"renderer\",\n        sid: sessionId,\n        ts: Date.now(),\n        type: `app-${AppConfig.version}`,\n        bid: buildId,\n        props: { msg, stack, src, line, col },\n    });\n    schedule();\n};\n"
  },
  {
    "path": "electron/mapi/log/beacon.ts",
    "content": "/**\n * 主进程异常上报（HTTP Beacon）\n * 仅在 isPackaged（非开发）模式下上报，批量异步发送。\n */\nimport https from \"node:https\";\nimport { AppConfig } from \"../../../src/config\";\nimport { isPackaged, platformUUID } from \"../../lib/env\";\n\ndeclare const __BUILD_ID__: string;\n\nconst BEACON_URL = \"https://g.tecmz.com/grow/load.gif\";\nconst BEACON_APP = \"focusany\";\nconst sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\nconst buildId = typeof __BUILD_ID__ !== \"undefined\" ? __BUILD_ID__ : \"unknown\";\n\ninterface BeaconEvent {\n    et: \"error\";\n    path: string;\n    did: string;\n    sid: string;\n    ts: number;\n    type: string;\n    bid: string;\n    props: { msg: string; stack?: string };\n}\n\nlet pending: BeaconEvent[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\nlet _did: string | null = null;\n\nconst getDid = (): string => {\n    if (_did) return _did;\n    try {\n        _did = platformUUID() || \"unknown\";\n    } catch {\n        _did = \"unknown\";\n    }\n    return _did;\n};\n\nconst flush = () => {\n    if (!pending.length) return;\n    const events = pending.splice(0);\n    try {\n        const encoded = encodeURIComponent(\n            Buffer.from(JSON.stringify(events)).toString(\"base64\"),\n        );\n        const url = `${BEACON_URL}?app=${BEACON_APP}&data=${encoded}`;\n        https.get(url).on(\"error\", () => {});\n    } catch {}\n};\n\nconst schedule = () => {\n    if (timer) return;\n    timer = setTimeout(() => {\n        timer = null;\n        flush();\n    }, 3000);\n};\n\nexport const reportError = (msg: string, stack?: string, path = \"/main\") => {\n    if (!isPackaged) return;\n    pending.push({\n        et: \"error\",\n        path,\n        did: getDid(),\n        sid: sessionId,\n        ts: Date.now(),\n        type: `app-${AppConfig.version}`,\n        bid: buildId,\n        props: { msg, stack },\n    });\n    schedule();\n};\n"
  },
  {
    "path": "electron/mapi/log/index.ts",
    "content": "import electron from \"electron\";\nimport date from \"date-and-time\";\nimport path from \"node:path\";\nimport { AppEnv } from \"../env\";\nimport fs from \"node:fs\";\nimport dayjs from \"dayjs\";\nimport FileIndex from \"../file\";\n\nlet fileName = null;\nlet fileStream = null;\nlet appFileNames = {};\nlet appFileStreams = {};\n\nconst stringDatetime = () => {\n    return date.format(new Date(), \"YYYYMMDD\");\n};\n\nconst jsonStringifyLogData = (data: any) => {\n    return JSON.stringify(data, (key, value) => {\n        if (typeof value === \"string\" && value.length > 200) {\n            if (\n                value.startsWith(\"data:\") ||\n                value.substring(0, 190).match(/^[a-zA-Z0-9+/=]+\\s*$/)\n            ) {\n                return (\n                    value.substring(0, 100) + \"...(length=\" + value.length + \")\"\n                );\n            }\n        }\n        return value;\n    });\n};\n\nconst logsDir = () => {\n    return path.join(AppEnv.userData, \"logs\");\n};\n\nconst appLogsDir = () => {\n    return path.join(AppEnv.dataRoot, \"logs\");\n};\n\nconst root = () => {\n    return logsDir();\n};\n\nconst file = () => {\n    return path.join(logsDir(), \"log_\" + stringDatetime() + \".log\");\n};\n\nconst appFile = (name: string) => {\n    return path.join(appLogsDir(), name + \"_\" + stringDatetime() + \".log\");\n};\n\nconst cleanOldLogs = (keepDays: number) => {\n    const logDirs = [\n        // 系统日志\n        logsDir(),\n        // 应用日志\n        appLogsDir(),\n    ];\n    for (const logDir of logDirs) {\n        if (!fs.existsSync(logDir)) {\n            return;\n        }\n        const files = fs.readdirSync(logDir);\n        const now = new Date();\n        // console.log('cleanOldLogs', logDir, files)\n        for (let file of files) {\n            const filePath = path.join(logDir, file);\n            let date = null;\n            for (let s of file.split(/[_\\\\.]/)) {\n                // 匹配 YYYYMMDD\n                if (s.match(/^\\d{8}$/)) {\n                    date = s;\n                    break;\n                }\n            }\n            if (!date) {\n                continue;\n            }\n            const fileDate = new Date(\n                parseInt(date.substring(0, 4)),\n                parseInt(date.substring(4, 6)) - 1,\n                parseInt(date.substring(6, 8)),\n            );\n            const diff = Math.abs(now.getTime() - fileDate.getTime());\n            const diffDays = Math.ceil(diff / (1000 * 3600 * 24));\n            // console.log('fileDate', file, fileDate, diffDays)\n            if (diffDays > keepDays) {\n                fs.unlinkSync(filePath);\n            }\n        }\n    }\n};\n\nconst log = (level: \"INFO\" | \"ERROR\", label: string, data: any = null) => {\n    if (fileName !== file()) {\n        fileName = file();\n        const logDir = logsDir();\n        if (!fs.existsSync(logDir)) {\n            fs.mkdirSync(logDir);\n        }\n        if (fileStream) {\n            fileStream.end();\n        }\n        fileStream = fs.createWriteStream(fileName, { flags: \"a\" });\n        cleanOldLogs(14);\n    }\n    let line = [];\n    line.push(date.format(new Date(), \"YYYY-MM-DD HH:mm:ss\"));\n    line.push(level);\n    line.push(label);\n    if (data) {\n        if (![\"number\", \"string\"].includes(typeof data)) {\n            data = jsonStringifyLogData(data);\n        }\n        line.push(data);\n    }\n    console.log(line.join(\" - \"));\n    fileStream.write(line.join(\" - \") + \"\\n\");\n};\n\nconst info = (label: string, data: any = null) => {\n    return log(\"INFO\", label, data);\n};\nconst error = (label: string, data: any = null) => {\n    return log(\"ERROR\", label, data);\n};\n\nconst appLog = (\n    name: string,\n    level: \"INFO\" | \"ERROR\",\n    label: string,\n    data: any = null,\n) => {\n    let fileChanged = false;\n    if (appFileNames[name] !== appFile(name)) {\n        appFileNames[name] = appFile(name);\n        fileChanged = true;\n    }\n    if (fileChanged || !appFileStreams[name]) {\n        if (appFileStreams[name]) {\n            appFileStreams[name].end();\n        }\n        const logDir = appLogsDir();\n        if (!fs.existsSync(logDir)) {\n            fs.mkdirSync(logDir);\n        }\n        appFileStreams[name] = fs.createWriteStream(appFileNames[name], {\n            flags: \"a\",\n        });\n    }\n    let line = [];\n    line.push(date.format(new Date(), \"YYYY-MM-DD HH:mm:ss\"));\n    line.push(level);\n    line.push(label);\n    if (data) {\n        if (![\"number\", \"string\"].includes(typeof data)) {\n            data = JSON.stringify(data);\n        }\n        line.push(data);\n    }\n    console.log(`[APP:${name}] - ` + line.join(\" - \"));\n    appFileStreams[name].write(line.join(\" - \") + \"\\n\");\n};\n\nconst appPath = (name: string) => {\n    if (!appFileNames[name]) {\n        appFileNames[name] = appFile(name);\n    }\n    return appFileNames[name];\n};\n\nconst appInfo = (name: string, label: string, data: any = null) => {\n    return appLog(name, \"INFO\", label, data);\n};\nconst appError = (name: string, label: string, data: any = null) => {\n    return appLog(name, \"ERROR\", label, data);\n};\n\nconst infoRenderOrMain = (label: string, data: any = null) => {\n    if (electron.ipcRenderer) {\n        console.log(\"Log.info\", label, data);\n        return electron.ipcRenderer.invoke(\"log:info\", label, data);\n    } else {\n        return info(label, data);\n    }\n};\nconst errorRenderOrMain = (label: string, data: any = null) => {\n    if (electron.ipcRenderer) {\n        console.error(\"Log.error\", label, data);\n        return electron.ipcRenderer.invoke(\"log:error\", label, data);\n    } else {\n        return error(label, data);\n    }\n};\n\nconst appInfoRenderOrMain = (name: string, label: string, data: any = null) => {\n    if (electron.ipcRenderer) {\n        console.log(\"Log.appInfo\", name, label, data);\n        return electron.ipcRenderer.invoke(\"log:appInfo\", name, label, data);\n    } else {\n        return appInfo(name, label, data);\n    }\n};\n\nconst appErrorRenderOrMain = (\n    name: string,\n    label: string,\n    data: any = null,\n) => {\n    if (electron.ipcRenderer) {\n        console.error(\"Log.appError\", name, label, data);\n        return electron.ipcRenderer.invoke(\"log:appError\", name, label, data);\n    } else {\n        return appError(name, label, data);\n    }\n};\n\nconst collectRenderOrMain = async (option?: {\n    startTime?: string;\n    endTime?: string;\n    limit?: number;\n}) => {\n    option = Object.assign(\n        {\n            startTime: dayjs().subtract(1, \"day\").format(\"YYYY-MM-DD HH:mm:ss\"),\n            endTime: dayjs().format(\"YYYY-MM-DD HH:mm:ss\"),\n            limit: 10 * 10000,\n        },\n        option,\n    );\n    let startMs = dayjs(option.startTime).valueOf();\n    let endMs = dayjs(option.endTime).valueOf();\n    let startDayMs = dayjs(option.startTime).startOf(\"day\").valueOf();\n    let endDayMs = dayjs(option.endTime).endOf(\"day\").valueOf();\n    let resultLines = [];\n    let logFiles = [];\n    logFiles = logFiles.concat(\n        await FileIndex.list(logsDir(), { isDataPath: false }),\n    );\n    logFiles = logFiles.concat(\n        await FileIndex.list(appLogsDir(), { isDataPath: false }),\n    );\n    // console.log('logFiles', logFiles)\n    logFiles = logFiles.filter((logFile) => {\n        if (logFile.isDirectory) {\n            return false;\n        }\n        let date = null;\n        for (let s of logFile.name.split(/[_\\\\.]/)) {\n            // 匹配 YYYYMMDD\n            if (s.match(/^\\d{8}$/)) {\n                date = s;\n                break;\n            }\n        }\n        if (!date) {\n            return false;\n        }\n        const fileDate = new Date(\n            parseInt(date.substring(0, 4)),\n            parseInt(date.substring(4, 6)) - 1,\n            parseInt(date.substring(6, 8)),\n        );\n        if (fileDate.getTime() < startDayMs || fileDate.getTime() > endDayMs) {\n            return false;\n        }\n        return true;\n    });\n    // console.log('collectRenderOrMain', {\n    //     ...option,\n    //     logFiles, startMs, endMs, startDayMs, endDayMs\n    // })\n    for (const logFile of logFiles) {\n        await FileIndex.readLine(\n            logFile.pathname,\n            (line) => {\n                const lineParts = line.split(\" - \");\n                const lineTime = dayjs(lineParts[0]);\n                // console.log('lineTime', lineParts[0], lineTime.isBefore(startMs) || lineTime.isAfter(endMs))\n                if (lineTime.isBefore(startMs) || lineTime.isAfter(endMs)) {\n                    return;\n                }\n                resultLines.push(line);\n            },\n            { isDataPath: false },\n        );\n    }\n    return {\n        startTime: option.startTime,\n        endTime: option.endTime,\n        logs: resultLines.join(\"\\n\"),\n    };\n};\n\nexport default {\n    root,\n    info,\n    error,\n    infoRenderOrMain,\n    errorRenderOrMain,\n    appPath,\n    appInfo,\n    appError,\n    appInfoRenderOrMain,\n    appErrorRenderOrMain,\n    collectRenderOrMain,\n    jsonStringifyLogData,\n};\n\nexport const Log = {\n    jsonStringifyLogData,\n    info: infoRenderOrMain,\n    error: errorRenderOrMain,\n    appPath,\n    appInfo: appInfoRenderOrMain,\n    appError: appErrorRenderOrMain,\n};\n"
  },
  {
    "path": "electron/mapi/log/main.ts",
    "content": "import { ipcMain } from \"electron\";\nimport logIndex from \"./index\";\n\nipcMain.handle(\"log:info\", (event, label: string, data: any) => {\n    logIndex.info(label, data);\n});\nipcMain.handle(\"log:error\", (event, label: string, data: any) => {\n    logIndex.error(label, data);\n});\nipcMain.handle(\n    \"log:appInfo\",\n    (event, name: string, label: string, data: any) => {\n        logIndex.appInfo(name, label, data);\n    },\n);\nipcMain.handle(\n    \"log:appError\",\n    (event, name: string, label: string, data: any) => {\n        logIndex.appError(name, label, data);\n    },\n);\n\nexport default {\n    info: logIndex.info,\n    error: logIndex.error,\n    appInfo: logIndex.appInfo,\n    appError: logIndex.appError,\n};\n\nexport const Log = {\n    info: logIndex.info,\n    error: logIndex.error,\n    appPath: logIndex.appPath,\n    appInfo: logIndex.appInfo,\n    appError: logIndex.appError,\n    jsonStringifyLogData: logIndex.jsonStringifyLogData,\n};\n"
  },
  {
    "path": "electron/mapi/log/render.ts",
    "content": "import logIndex from \"./index\";\n\nexport default {\n    root: logIndex.root,\n    info: logIndex.infoRenderOrMain,\n    error: logIndex.errorRenderOrMain,\n    appInfo: logIndex.appInfoRenderOrMain,\n    appError: logIndex.appErrorRenderOrMain,\n    collect: logIndex.collectRenderOrMain,\n};\n"
  },
  {
    "path": "electron/mapi/main.ts",
    "content": "import app from \"./app/main\";\nimport config from \"./config/main\";\nimport db from \"./db/main\";\nimport event from \"./event/main\";\nimport file from \"./file/main\";\nimport { HttpServer } from \"./httpserver/main\";\nimport keys from \"./keys/main\";\nimport kvdb from \"./kvdb/main\";\nimport log from \"./log/main\";\nimport manager from \"./manager/main\";\nimport misc from \"./misc/main\";\nimport protocol from \"./protocol/main\";\nimport storage from \"./storage/main\";\nimport ui from \"./ui\";\nimport updater from \"./updater/main\";\nimport user from \"./user/main\";\n\nconst $mapi = {\n    app,\n    log,\n    config,\n    storage,\n    db,\n    file,\n    event,\n    ui,\n    keys,\n    user,\n    misc,\n    protocol,\n    updater,\n    manager,\n    kvdb,\n};\n\nexport const MAPI = {\n    async init() {\n        await $mapi.user.init();\n        await $mapi.db.init();\n        await $mapi.event.init();\n        $mapi.kvdb.init();\n        $mapi.manager.init();\n        HttpServer.start().then();\n    },\n    ready() {\n        $mapi.keys.ready();\n        $mapi.manager.ready();\n        $mapi.protocol.ready();\n    },\n    destroy() {\n        $mapi.keys.destroy();\n        $mapi.manager.destroy();\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/automation/index.ts",
    "content": "import { Button, Key, keyboard, mouse } from \"@nut-tree-fork/nut-js\";\nimport { activeWindow, Result } from \"get-windows\";\nimport { windowManager } from \"node-window-manager\";\nimport { Window } from \"node-window-manager/src/classes/window\";\nimport { ActiveWindow, ClipboardDataType } from \"../../../../src/types/Manager\";\nimport { Log } from \"../../log/main\";\nimport { ManagerClipboard } from \"../clipboard\";\n\nexport const ManagerAutomation = {\n    init() {\n        ManagerAutomation.track();\n    },\n    lastWindow: null as Result | null,\n    lastWindowManager: null as Window | null,\n    track() {\n        windowManager.on(\"window-activated\", async (win) => {\n            if (win) {\n                if (\n                    !ManagerAutomation.lastWindow ||\n                    win.id !== ManagerAutomation.lastWindow.id\n                ) {\n                    if (!ManagerAutomation.trackShouldIgnore(win)) {\n                        ManagerAutomation.lastWindow = win;\n                        ManagerAutomation.lastWindowManager =\n                            windowManager.getActiveWindow();\n                    }\n                }\n            }\n        });\n    },\n    trackShouldIgnore(win: Window): boolean {\n        if (!win || !win.id) {\n            return true;\n        }\n        if ([\"Electron\", \"FocusAny\"].includes(win.getTitle())) {\n            return true;\n        }\n        // if (['FocusAny'].includes(win.getOwner()?.name)) {\n        //     return true;\n        // }\n        Log.info(\"ManagerAutomation.track\", {\n            win,\n            title: win.getTitle(),\n            owner: win.getOwner(),\n        });\n        return false;\n    },\n    async activateLatestWindow(): Promise<void> {\n        if (ManagerAutomation.lastWindowManager) {\n            ManagerAutomation.lastWindowManager.bringToTop();\n        }\n    },\n    async getActiveWindow(): Promise<ActiveWindow> {\n        const win = {\n            name: \"\",\n            title: \"\",\n            attr: {},\n            raw: null,\n        } as ActiveWindow;\n        const active = await activeWindow();\n        if (active) {\n            win.raw = active;\n            win.name = active.owner?.name || \"\";\n            win.title = active.title;\n            if (\"url\" in active) {\n                win.attr[\"url\"] = active.url + \"\";\n            }\n        }\n        return win;\n    },\n    async typeString(text: string): Promise<void> {\n        // await keyboard.type(text);\n        await ManagerClipboard.pasteClipboardContent({\n            type: \"text\",\n            text: text,\n        } as ClipboardDataType);\n    },\n    async typeKey(key: string): Promise<void> {\n        const keyMap: { [key: string]: Key } = {\n            a: Key.A,\n            b: Key.B,\n            c: Key.C,\n            d: Key.D,\n            e: Key.E,\n            f: Key.F,\n            g: Key.G,\n            h: Key.H,\n            i: Key.I,\n            j: Key.J,\n            k: Key.K,\n            l: Key.L,\n            m: Key.M,\n            n: Key.N,\n            o: Key.O,\n            p: Key.P,\n            q: Key.Q,\n            r: Key.R,\n            s: Key.S,\n            t: Key.T,\n            u: Key.U,\n            v: Key.V,\n            w: Key.W,\n            x: Key.X,\n            y: Key.Y,\n            z: Key.Z,\n            \"0\": Key.Num0,\n            \"1\": Key.Num1,\n            \"2\": Key.Num2,\n            \"3\": Key.Num3,\n            \"4\": Key.Num4,\n            \"5\": Key.Num5,\n            \"6\": Key.Num6,\n            \"7\": Key.Num7,\n            \"8\": Key.Num8,\n            \"9\": Key.Num9,\n            space: Key.Space,\n            enter: Key.Enter,\n            tab: Key.Tab,\n            backspace: Key.Backspace,\n            delete: Key.Delete,\n            escape: Key.Escape,\n            shift: Key.LeftShift,\n            control: Key.LeftControl,\n            alt: Key.LeftAlt,\n            command: Key.LeftSuper,\n            left: Key.Left,\n            right: Key.Right,\n            up: Key.Up,\n            down: Key.Down,\n            f1: Key.F1,\n            f2: Key.F2,\n            f3: Key.F3,\n            f4: Key.F4,\n            f5: Key.F5,\n            f6: Key.F6,\n            f7: Key.F7,\n            f8: Key.F8,\n            f9: Key.F9,\n            f10: Key.F10,\n            f11: Key.F11,\n            f12: Key.F12,\n        };\n        const nutKey = keyMap[key.toLowerCase()];\n        if (nutKey) {\n            await keyboard.pressKey(nutKey);\n        }\n    },\n    async mouseToggle(\n        type: \"down\" | \"up\",\n        button: \"left\" | \"right\" | \"middle\",\n    ): Promise<void> {\n        const buttonMap: { [key: string]: Button } = {\n            left: Button.LEFT,\n            right: Button.RIGHT,\n            middle: Button.MIDDLE,\n        };\n        const nutButton = buttonMap[button];\n        if (nutButton) {\n            if (type === \"down\") {\n                await mouse.pressButton(nutButton);\n            } else {\n                await mouse.releaseButton(nutButton);\n            }\n        }\n    },\n    async moveMouse(x: number, y: number): Promise<void> {\n        await mouse.setPosition({ x, y });\n    },\n    async mouseClick(\n        button: \"left\" | \"right\" | \"middle\",\n        double: boolean = false,\n    ): Promise<void> {\n        const buttonMap: { [key: string]: Button } = {\n            left: Button.LEFT,\n            right: Button.RIGHT,\n            middle: Button.MIDDLE,\n        };\n        const nutButton = buttonMap[button];\n        if (nutButton) {\n            if (double) {\n                await mouse.doubleClick(nutButton);\n            } else {\n                await mouse.click(nutButton);\n            }\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/backend/index.ts",
    "content": "import { ActionRecord, PluginRecord } from \"../../../../src/types/Manager\";\nimport { ManagerSystem } from \"../system\";\nimport fs from \"node:fs\";\nimport { ImportUtil } from \"../../../lib/util\";\nimport { PluginSdkCreate } from \"../plugin/sdk\";\nimport { PluginLog } from \"../plugin/log\";\n\nexport const ManagerBackend = {\n    async run(\n        plugin: PluginRecord,\n        type: \"hook\" | \"event\" | \"action\" | \"mcpTool\",\n        key: string,\n        data: any,\n        option?: {\n            rejectIfError: boolean;\n        },\n    ) {\n        option = Object.assign(\n            {\n                rejectIfError: false,\n            },\n            option,\n        );\n        try {\n            if (!plugin.runtime?.root) {\n                throw `PluginRootNotFound:${plugin.name}:${type}:${key}`;\n            }\n            const backendPath = `${plugin.runtime?.root}/backend.cjs`;\n            if (!fs.existsSync(backendPath)) {\n                if (option.rejectIfError) {\n                    throw `BackendFileNotFound:${backendPath}`;\n                }\n                return;\n            }\n            const backend = await ImportUtil.loadCommonJs(backendPath);\n            if (!(type in backend)) {\n                if (option.rejectIfError) {\n                    throw `BackendTypeNotFound:${type}`;\n                }\n                return;\n            }\n            if (!(key in backend[type])) {\n                if (option.rejectIfError) {\n                    throw `BackendKeyNotFound:${type}.${key}`;\n                }\n                return;\n            }\n            const func = backend[type][key];\n            const sdk = PluginSdkCreate(plugin);\n            return await new Promise((resolve, reject) => {\n                Promise.resolve(func(sdk, data)).then(resolve).catch(reject);\n            });\n        } catch (e) {\n            PluginLog.error(plugin.name, `Backend.Run.Error-${type}-${key}`, {\n                error: e + \"\",\n                data,\n                option,\n            });\n        }\n    },\n    async runAction(plugin: PluginRecord, action: ActionRecord, option?: {}) {\n        const codeData = {};\n        codeData[\"actionName\"] = action.name;\n        codeData[\"actionMatch\"] = action.runtime?.match;\n        try {\n            const callback = ManagerSystem.getActionBackendFunc(\n                plugin.name,\n                action.name,\n            );\n            if (callback) {\n                return await callback(codeData);\n            }\n            return await this.run(plugin, \"action\", action.name, codeData, {\n                rejectIfError: true,\n            });\n        } catch (e) {\n            PluginLog.error(\n                plugin.name,\n                `Backend.RunAction.Error:${action.name}`,\n                e + \"\",\n            );\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/clipboard/clipboardFiles.ts",
    "content": "import { clipboard } from \"electron\";\nimport plist from \"plist\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport ofs from \"original-fs\";\nimport { isLinux, isMac, isWin } from \"../../../lib/env\";\n\nlet electronClipboardEx = null;\nif (isMac || isWin) {\n    (async () => {\n        try {\n            electronClipboardEx = await import(\"electron-clipboard-ex\");\n            electronClipboardEx = electronClipboardEx.default;\n        } catch (e) {}\n    })();\n}\n\nexport const getClipboardFiles = (): FileItem[] => {\n    let fileInfo: any;\n    if (isMac) {\n        if (!clipboard.has(\"NSFilenamesPboardType\")) {\n            return [];\n        }\n        const result = clipboard.read(\"NSFilenamesPboardType\");\n        if (!result) {\n            return [];\n        }\n        try {\n            fileInfo = plist.parse(result);\n        } catch (e) {\n            return [];\n        }\n    } else if (isWin) {\n        try {\n            /* eslint-disable */\n            fileInfo = electronClipboardEx.readFilePaths();\n        } catch (e) {\n            // todo\n        }\n    } else if (isLinux) {\n        if (!clipboard.has(\"text/uri-list\")) {\n            return [];\n        }\n        const result = clipboard\n            .read(\"text/uri-list\")\n            .match(/^file:\\/\\/\\/.*/gm);\n        if (!result || !result.length) {\n            return [];\n        }\n        fileInfo = result.map((e) =>\n            decodeURIComponent(e).replace(/^file:\\/\\//, \"\"),\n        );\n    }\n    if (!Array.isArray(fileInfo)) {\n        return [];\n    }\n    const target: any = fileInfo\n        .map((p) => {\n            if (!fs.existsSync(p)) return false;\n            let info;\n            try {\n                info = ofs.lstatSync(p);\n            } catch (e) {\n                return false;\n            }\n            let fileExt = null;\n            if (info.isFile()) {\n                fileExt = path.extname(p).toLowerCase().replace(/^./, \"\");\n            }\n            return {\n                isFile: info.isFile(),\n                isDirectory: info.isDirectory(),\n                name: path.basename(p) || p,\n                path: p,\n                fileExt: fileExt,\n            };\n        })\n        .filter(Boolean);\n    return target.length ? target : [];\n};\n\nexport const setClipboardFiles = (files: string[]) => {\n    if (!files || !files.length) {\n        return;\n    }\n    if (isMac) {\n        clipboard.writeBuffer(\n            \"NSFilenamesPboardType\",\n            Buffer.from(plist.build(files)),\n        );\n    } else if (isWin) {\n        electronClipboardEx.writeFilePaths(files);\n    } else if (isLinux) {\n        // @ts-ignore\n        clipboard.write(\n            \"text/uri-list\",\n            files.map((e) => `file://${e}`).join(\"\\n\"),\n        );\n    }\n};\n"
  },
  {
    "path": "electron/mapi/manager/clipboard/index.ts",
    "content": "import { AppsMain } from \"../../app/main\";\nimport {\n    ClipboardDataType,\n    ClipboardHistoryRecord,\n} from \"../../../../src/types/Manager\";\nimport { Files } from \"../../file/main\";\nimport {\n    EncodeUtil,\n    FileUtil,\n    sleep,\n    StrUtil,\n    TimeUtil,\n} from \"../../../lib/util\";\nimport StorageMain from \"../../storage/main\";\nimport { getClipboardFiles, setClipboardFiles } from \"./clipboardFiles\";\nimport { clipboard } from \"electron\";\nimport { isMac } from \"../../../lib/env\";\nimport { KeyboardKey, ManagerHotkeySimulate } from \"../hotkey/simulate\";\nimport { ManagerPluginEvent } from \"../plugin/event\";\nimport { Log } from \"../../log/main\";\n\nexport const ManagerClipboard = {\n    MAX_LIMIT: 1000,\n    running: true,\n    interval: 1000,\n    timer: null,\n    watchNextTime: 0,\n    lastContentJson: null,\n    lastChangeTimestamp: 0,\n    encryptKey: null,\n    clipboardBusy: false,\n    clipboardBackupData: null as ClipboardDataType | null,\n    async init() {\n        this.encryptKey = await StorageMain.get(\n            \"clipboard\",\n            \"encryptKey\",\n            null,\n        );\n        if (!this.encryptKey) {\n            this.encryptKey = StrUtil.randomString(16);\n            await StorageMain.set(\"clipboard\", \"encryptKey\", this.encryptKey);\n        }\n        this.monitorStart();\n        // console.log('all', await this.list())\n    },\n    async waitClipboardFree() {\n        while (this.clipboardBusy) {\n            await sleep(10);\n        }\n    },\n    async backupClipboard() {\n        await this.waitClipboardFree();\n        this.clipboardBusy = true;\n        this.clipboardBackupData = await this._getClipboardContent();\n        clipboard.clear();\n    },\n    async restoreClipboard() {\n        clipboard.clear();\n        if (this.clipboardBackupData) {\n            await this._setClipboardContent(this.clipboardBackupData);\n            this.clipboardBackupData = null;\n        }\n        this.clipboardBusy = false;\n    },\n    async getSelectedContent(): Promise<ClipboardDataType | null> {\n        await this.backupClipboard();\n        ManagerHotkeySimulate.keyTap(KeyboardKey.C, [\n            isMac ? KeyboardKey.Meta : KeyboardKey.Ctrl,\n        ]);\n        await new Promise((resolve) => setTimeout(resolve, 200));\n        const select = await this._getClipboardContent();\n        await this.restoreClipboard();\n        return select;\n    },\n\n    async _setClipboardContent(data: ClipboardDataType): Promise<void> {\n        switch (data.type) {\n            case \"file\":\n                setClipboardFiles(data.files.map((file) => file.path));\n                break;\n            case \"image\":\n                AppsMain.setClipboardImage(data.image);\n                break;\n            case \"text\":\n                AppsMain.setClipboardText(data.text);\n                break;\n        }\n    },\n    async _getClipboardContent(): Promise<ClipboardDataType | null> {\n        const files = getClipboardFiles();\n        if (files.length) {\n            return {\n                type: \"file\",\n                files: files,\n            } as ClipboardDataType;\n        }\n        const image = AppsMain.getClipboardImage();\n        if (image) {\n            return {\n                type: \"image\",\n                image: image,\n            } as ClipboardDataType;\n        }\n        const text = AppsMain.getClipboardText();\n        if (text) {\n            return {\n                type: \"text\",\n                text: text,\n            } as ClipboardDataType;\n        }\n        return null;\n    },\n    async pasteClipboardContent(data: ClipboardDataType): Promise<void> {\n        if (!data) {\n            return;\n        }\n        await this.backupClipboard();\n        await this._setClipboardContent(data);\n        ManagerHotkeySimulate.keyTap(KeyboardKey.V, [\n            isMac ? KeyboardKey.Meta : KeyboardKey.Ctrl,\n        ]);\n        await sleep(200);\n        await this.restoreClipboard();\n    },\n    async getClipboardContent(): Promise<ClipboardDataType | null> {\n        await this.waitClipboardFree();\n        const content = await this._getClipboardContent();\n        this.watchNextTime = Date.now() + this.interval;\n        const contentJson = JSON.stringify(content);\n        if (\n            null == this.lastContentJson ||\n            contentJson !== this.lastContentJson\n        ) {\n            if (this.lastContentJson) {\n                this.lastChangeTimestamp = TimeUtil.timestamp();\n            }\n            this.lastContentJson = contentJson;\n            this.onChange(content).then();\n        }\n        return content;\n    },\n    _watch() {\n        if (this.watchNextTime > Date.now()) {\n            setTimeout(\n                () => {\n                    this._watch();\n                },\n                Math.max(this.watchNextTime - Date.now(), 0),\n            );\n            return;\n        }\n        this.getClipboardContent().finally(() => {\n            if (this.running) {\n                setTimeout(\n                    () => {\n                        this._watch();\n                    },\n                    Math.max(this.watchNextTime - Date.now(), 0),\n                );\n            }\n        });\n    },\n    monitorStart() {\n        this.running = true;\n        this._watch();\n    },\n    monitorStop() {\n        this.running = false;\n    },\n    encrypt(data: ClipboardHistoryRecord) {\n        const dataJson = JSON.stringify(data);\n        return EncodeUtil.aesEncode(dataJson, this.encryptKey);\n    },\n    decrypt(data: string): ClipboardHistoryRecord {\n        try {\n            data = EncodeUtil.aesDecode(data, this.encryptKey);\n            return JSON.parse(data) as ClipboardHistoryRecord;\n        } catch (e) {\n            return null;\n        }\n    },\n    async onChange(data: ClipboardDataType) {\n        if (!data) {\n            return;\n        }\n        // console.log('clipboard.onChange', data)\n        const filename = TimeUtil.timestampDayStart();\n        const saveData = {\n            type: data.type,\n            timestamp: TimeUtil.timestamp(),\n            files: data.files,\n            image: data.image,\n            text: data.text,\n        } as ClipboardHistoryRecord;\n        if (saveData.image) {\n            const imageMd5 = EncodeUtil.md5(saveData.image);\n            let imageFile = `clipboard/${filename}/${imageMd5}`;\n            Files.writeBuffer(\n                imageFile,\n                FileUtil.base64ToBuffer(saveData.image),\n                { isDataPath: true },\n            ).then();\n            saveData.image = imageMd5;\n        }\n        const dataString = this.encrypt(saveData);\n        // console.log('clipboard.write', `clipboard/${filename}/data`, dataString)\n        await Files.appendText(\n            `clipboard/${filename}/data`,\n            `${dataString}\\n`,\n            { isDataPath: true },\n        );\n        await ManagerPluginEvent.firePluginEvent(\"ClipboardChange\", saveData);\n    },\n    async list(limit: number = -1): Promise<ClipboardHistoryRecord[]> {\n        const fullPath = await Files.fullPath(\"clipboard\");\n        const dateDir = await Files.list(\"clipboard\", { isDataPath: true });\n        // 按照倒序排列 pathname\n        dateDir.sort((a, b) => {\n            return b.pathname.localeCompare(a.pathname);\n        });\n        const result = [];\n        let maxLimitReached = false;\n        for (const dir of dateDir) {\n            if (maxLimitReached) {\n                await Files.deletes(`clipboard/${dir.name}`, {\n                    isDataPath: true,\n                });\n                continue;\n            }\n            const data = await Files.read(`clipboard/${dir.name}/data`, {\n                isDataPath: true,\n            });\n            if (!data) {\n                await Files.deletes(`clipboard/${dir.name}`, {\n                    isDataPath: true,\n                });\n                Log.error(\n                    \"ManagerClipboard.list\",\n                    `Deleted empty clipboard directory: clipboard/${dir.name}`,\n                );\n                continue;\n            }\n            for (const line of data.split(\"\\n\").reverse()) {\n                if (!line) {\n                    continue;\n                }\n                const record = this.decrypt(line);\n                if (!record) {\n                    continue;\n                }\n                if (record.image) {\n                    record.image = `file://${fullPath}/${dir.name}/${record.image}`;\n                }\n                result.push(record);\n                if (limit > 0 && result.length >= limit) {\n                    break;\n                }\n                if (result.length > ManagerClipboard.MAX_LIMIT) {\n                    maxLimitReached = true;\n                    break;\n                }\n            }\n            if (limit > 0 && result.length >= limit) {\n                break;\n            }\n        }\n        return result;\n    },\n    async clear() {\n        await Files.deletes(\"clipboard\", { isDataPath: true });\n    },\n    async delete(timestamp: number) {\n        const date = TimeUtil.timestampDayStart(timestamp * 1000);\n        const data = await Files.read(`clipboard/${date}/data`, {\n            isDataPath: true,\n        });\n        const lines = data.split(\"\\n\");\n        const result = [];\n        for (const line of lines) {\n            if (!line) {\n                continue;\n            }\n            const record = this.decrypt(line);\n            if (!record) {\n                continue;\n            }\n            if (record.timestamp !== timestamp) {\n                result.push(line);\n            }\n        }\n        await Files.write(`clipboard/${date}/data`, result.join(\"\\n\"), {\n            isDataPath: true,\n        });\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/code/index.ts",
    "content": "import { ActionRecord, PluginRecord } from \"../../../../src/types/Manager\";\nimport { ManagerSystem } from \"../system\";\nimport { ManagerWindow } from \"../window\";\nimport { PluginLog } from \"../plugin/log\";\n\nexport const ManagerCode = {\n    async execute(plugin: PluginRecord, action: ActionRecord, option?: {}) {\n        try {\n            const codeData = {};\n            codeData[\"actionName\"] = action.name;\n            codeData[\"actionMatch\"] = action.runtime?.match;\n            codeData[\"requestId\"] = action.runtime?.requestId;\n            const callback = ManagerSystem.getActionCodeFunc(\n                plugin.name,\n                action.name,\n            );\n            if (callback) {\n                return await callback(codeData);\n            }\n            return await ManagerWindow.openForCode(plugin, action, {\n                codeData,\n            });\n        } catch (e) {\n            PluginLog.error(plugin.name, `Code.Execute.Error`, {\n                error: e + \"\",\n                action,\n                option,\n            });\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/config/config.ts",
    "content": "import {\n    ActionRecord,\n    ConfigRecord,\n    LaunchRecord,\n    PluginActionRecord,\n    PluginConfig,\n    PluginRecord,\n} from \"../../../../src/types/Manager\";\n\nimport { KVDBMain } from \"../../kvdb/main\";\nimport { CommonConfig } from \"../../../config/common\";\nimport { ManagerHotkey } from \"../hotkey\";\nimport { MemoryCacheUtil } from \"../../../lib/util\";\nimport { ManagerPlugin } from \"../plugin\";\nimport { ManagerSystem } from \"../system\";\nimport { isLinux, isMac, isWin } from \"../../../lib/env\";\n\nconst defaultConfig: ConfigRecord = {\n    mainTrigger: {\n        key: \"Space\",\n        altKey: true,\n        ctrlKey: false,\n        metaKey: false,\n        shiftKey: false,\n        times: 1,\n    },\n    detachWindowTrigger: {\n        key: \"D\",\n        altKey: false,\n        ctrlKey: !isMac,\n        metaKey: isMac,\n        shiftKey: false,\n        times: 1,\n    },\n    fastPanelTrigger: {\n        type: \"Ctrl\",\n        times: 1,\n    },\n    // fastPanelTriggerButton: {\n    //     button: HotkeyMouseButtonEnum.RIGHT,\n    //     type: 'longPress',\n    // },\n};\n\nexport const ManagerConfig = {\n    configOld: null as ConfigRecord | null,\n    async clearCache() {\n        MemoryCacheUtil.forget(\"Config\");\n        MemoryCacheUtil.forget(\"DisabledActionMatches\");\n        MemoryCacheUtil.forget(\"PinActions\");\n        MemoryCacheUtil.forget(\"Launches\");\n        MemoryCacheUtil.forget(\"CustomActions\");\n        MemoryCacheUtil.forget(\"HistoryActions\");\n        MemoryCacheUtil.forget(\"PluginConfig\");\n    },\n    async get(): Promise<ConfigRecord> {\n        return MemoryCacheUtil.remember(\"Config\", async () => {\n            // reset config\n            // await this.save(defaultConfig)\n            const config = await KVDBMain.getData(\n                CommonConfig.dbSystem,\n                CommonConfig.dbConfigId,\n            );\n            if (!config) {\n                await this.save(defaultConfig);\n                this.configOld = defaultConfig;\n                return defaultConfig;\n            }\n            let changed = false;\n            for (const key in defaultConfig) {\n                if (key in config) {\n                    if (typeof config[key] === \"object\") {\n                        for (const subKey in defaultConfig[key]) {\n                            if (subKey in config[key]) {\n                            } else {\n                                config[key][subKey] =\n                                    defaultConfig[key][subKey];\n                                changed = true;\n                            }\n                        }\n                    }\n                } else {\n                    config[key] = defaultConfig[key];\n                    changed = true;\n                }\n            }\n            if (changed) {\n                await this.save(config);\n            }\n            this.configOld = config;\n            return config;\n        });\n    },\n    async save(config: ConfigRecord): Promise<void> {\n        // delete config[\"data\"];\n        const doc = {\n            _id: CommonConfig.dbConfigId,\n            ...config,\n        };\n        await KVDBMain.putForce(CommonConfig.dbSystem, doc);\n        let hotkeyChanged = false;\n        if (this.configOld) {\n            for (const k of [\"mainTrigger\", \"fastPanelTrigger\"]) {\n                if (\n                    JSON.stringify(this.configOld[k]) !==\n                    JSON.stringify(config[k])\n                ) {\n                    hotkeyChanged = true;\n                    break;\n                }\n            }\n        }\n        MemoryCacheUtil.forget(\"Config\");\n        if (hotkeyChanged) {\n            ManagerHotkey.configInit().then();\n        }\n    },\n    async listDisabledActionMatch() {\n        return MemoryCacheUtil.remember(\"DisabledActionMatches\", async () => {\n            return (\n                (await KVDBMain.getData(\n                    CommonConfig.dbSystem,\n                    CommonConfig.dbDisabledActionMatchId,\n                )) || {}\n            );\n        });\n    },\n    async toggleDisabledActionMatch(\n        pluginName: string,\n        actionName: string,\n        matchName: string,\n    ) {\n        let matches = await this.listDisabledActionMatch();\n        if (!matches) {\n            matches = {};\n        }\n        if (!matches[pluginName]) {\n            matches[pluginName] = {};\n        }\n        if (!matches[pluginName][actionName]) {\n            matches[pluginName][actionName] = [];\n        }\n        let disabled = false;\n        if (matches[pluginName][actionName].includes(matchName)) {\n            matches[pluginName][actionName] = matches[pluginName][\n                actionName\n            ].filter((v) => v !== matchName);\n            if (!matches[pluginName][actionName].length) {\n                delete matches[pluginName][actionName];\n            }\n            if (!Object.keys(matches[pluginName]).length) {\n                delete matches[pluginName];\n            }\n        } else {\n            matches[pluginName][actionName].push(matchName);\n            disabled = true;\n        }\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbDisabledActionMatchId,\n            ...matches,\n        });\n        MemoryCacheUtil.forget(\"DisabledActionMatches\");\n        return disabled;\n    },\n    async listPinAction(): Promise<PluginActionRecord[]> {\n        return MemoryCacheUtil.remember(\"PinActions\", async () => {\n            const res = await KVDBMain.getData(\n                CommonConfig.dbSystem,\n                CommonConfig.dbPinActionId,\n            );\n            if (!res) {\n                return [];\n            }\n            return res[\"records\"] || [];\n        });\n    },\n    async getPinedActionSet(): Promise<Set<string>> {\n        const pinActions = await this.listPinAction();\n        const set = new Set<string>();\n        for (const pinAction of pinActions) {\n            set.add(`${pinAction.pluginName}/${pinAction.actionName}`);\n        }\n        return set;\n    },\n    async togglePinAction(pluginName: string, actionName: string) {\n        let pinActions = await this.listPinAction();\n        const saveAction = {\n            pluginName: pluginName,\n            actionName: actionName,\n        } as PluginActionRecord;\n        const exists = pinActions.find(\n            (v) =>\n                v.pluginName === saveAction.pluginName &&\n                v.actionName === saveAction.actionName,\n        );\n        if (exists) {\n            pinActions = pinActions.filter(\n                (v) =>\n                    v.pluginName !== saveAction.pluginName ||\n                    v.actionName !== saveAction.actionName,\n            );\n        } else {\n            pinActions.unshift(saveAction);\n        }\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbPinActionId,\n            records: pinActions,\n        });\n        MemoryCacheUtil.forget(\"PinActions\");\n    },\n    async listLaunch(): Promise<LaunchRecord[]> {\n        return MemoryCacheUtil.remember(\"Launches\", async () => {\n            const res = await KVDBMain.getData(\n                CommonConfig.dbSystem,\n                CommonConfig.dbLaunchId,\n            );\n            if (!res) {\n                return [];\n            }\n            return res[\"records\"] || [];\n        });\n    },\n    async updateLaunch(records: LaunchRecord[]) {\n        // normalize records\n        records.forEach((record) => {\n            if (!(\"type\" in record)) {\n                // @ts-ignore\n                record.type = \"custom\";\n            }\n            if (!(\"name\" in record)) {\n                // @ts-ignore\n                record.name = \"\";\n            }\n        });\n        // sort records by type(custom,plugin) and name(a-z)\n        records.sort((a, b) => {\n            if (a.type === b.type) {\n                return a.name.localeCompare(b.name);\n            }\n            return a.type.localeCompare(b.type);\n        });\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbLaunchId,\n            records: records,\n        });\n        MemoryCacheUtil.forget(\"Launches\");\n        ManagerHotkey.configInit().then();\n    },\n    async getCustomAction(): Promise<Record<string, ActionRecord[]>> {\n        return MemoryCacheUtil.remember(\"CustomActions\", async () => {\n            return (\n                (await KVDBMain.getData(\n                    CommonConfig.dbSystem,\n                    CommonConfig.dbCustomActionId,\n                )) || {}\n            );\n        });\n    },\n    async addCustomAction(\n        plugin: PluginRecord,\n        action: ActionRecord | ActionRecord[],\n    ) {\n        const customAction = await this.getCustomAction();\n        if (!(plugin.name in customAction)) {\n            customAction[plugin.name] = [];\n        }\n        if (!Array.isArray(action)) {\n            action = [action];\n        }\n        for (let a of action) {\n            a = ManagerPlugin.normalAction(a, plugin);\n            let replace = false;\n            for (let i = 0; i < customAction[plugin.name].length; i++) {\n                if (customAction[plugin.name][i].name === a.name) {\n                    customAction[plugin.name][i] = a;\n                    replace = true;\n                    break;\n                }\n            }\n            if (!replace) {\n                customAction[plugin.name].push(a);\n            }\n        }\n        await this.updateCustomAction(customAction);\n    },\n    async updateCustomAction(customAction: Record<string, ActionRecord[]>) {\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbCustomActionId,\n            ...customAction,\n        });\n        MemoryCacheUtil.forget(\"CustomActions\");\n    },\n    async removeCustomAction(plugin: PluginRecord, name: string) {\n        const customAction = await this.getCustomAction();\n        if (!(plugin.name in customAction)) {\n            return;\n        }\n        customAction[plugin.name] = customAction[plugin.name].filter(\n            (v) => v.name !== name,\n        );\n        await this.updateCustomAction(customAction);\n    },\n    async clearCustomAction(pluginName: string) {\n        const customAction = await this.getCustomAction();\n        if (!(pluginName in customAction)) {\n            return;\n        }\n        delete customAction[pluginName];\n        await this.updateCustomAction(customAction);\n    },\n    async getHistoryAction(): Promise<PluginActionRecord[]> {\n        return MemoryCacheUtil.remember(\"HistoryActions\", async () => {\n            const res = await KVDBMain.getData(\n                CommonConfig.dbSystem,\n                CommonConfig.dbHistoryActionId,\n            );\n            if (!res) {\n                return [];\n            }\n            return res[\"records\"] || [];\n        });\n    },\n    async clearHistoryAction() {\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbHistoryActionId,\n            records: [],\n        });\n        MemoryCacheUtil.forget(\"HistoryActions\");\n    },\n    async deleteHistoryAction(pluginName: string, actionName: string) {\n        // console.log('deleteHistoryAction', fullName)\n        let historyActions = await this.getHistoryAction();\n        historyActions = historyActions.filter(\n            (v) => v.pluginName !== pluginName || v.actionName !== actionName,\n        );\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbHistoryActionId,\n            records: historyActions,\n        });\n        MemoryCacheUtil.forget(\"HistoryActions\");\n    },\n    async addHistoryAction(plugin: PluginRecord, action: ActionRecord) {\n        let historyActions = await this.getHistoryAction();\n        const saveAction = {\n            pluginName: plugin.name,\n            actionName: action.name,\n        } as PluginActionRecord;\n        // remove duplicate\n        historyActions = historyActions.filter(\n            (v) =>\n                v.pluginName !== saveAction.pluginName ||\n                v.actionName !== saveAction.actionName,\n        );\n        historyActions.unshift(saveAction);\n        if (historyActions.length > 100) {\n            historyActions.pop();\n        }\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbHistoryActionId,\n            records: historyActions,\n        });\n        MemoryCacheUtil.forget(\"HistoryActions\");\n    },\n    async getPluginConfigAll(): Promise<Record<string, PluginConfig>> {\n        return MemoryCacheUtil.remember(\"PluginConfig\", async () => {\n            const res = await KVDBMain.getData(\n                CommonConfig.dbSystem,\n                CommonConfig.dbPluginConfigId,\n            );\n            if (!res) {\n                return {};\n            }\n            return res[\"records\"] || {};\n        });\n    },\n    async getPluginConfig(pluginName: string): Promise<PluginConfig> {\n        const res = await this.getPluginConfigAll();\n        return res[pluginName] || {};\n    },\n    async setPluginConfigItem(pluginName: string, key: string, value: any) {\n        const config = await this.getPluginConfig(pluginName);\n        config[key] = value;\n        await ManagerConfig.setPluginConfig(pluginName, config);\n        if (ManagerSystem.match(pluginName)) {\n            await ManagerSystem.clearCache();\n        } else {\n            await ManagerPlugin.clearCache();\n        }\n    },\n    async setPluginConfig(pluginName: string, config: PluginConfig) {\n        const pluginConfig = await this.getPluginConfigAll();\n        pluginConfig[pluginName] = config;\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbPluginConfigId,\n            records: pluginConfig,\n        });\n        MemoryCacheUtil.forget(\"PluginConfig\");\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/editor/index.ts",
    "content": "import nodePath from \"node:path\";\nimport { t } from \"../../../config/lang\";\nimport { AppsMain } from \"../../app/main\";\nimport { Files } from \"../../file/main\";\nimport { Log } from \"../../log/main\";\nimport { Manager } from \"../manager\";\nimport { SearchQuery } from \"../type\";\n\nexport const ManagerEditor = {\n    filePath: null,\n    isReady: false,\n    async init() {},\n    async ready() {\n        this.isReady = true;\n    },\n    faDataTypeCache: {},\n    async getFaDataTypeCached(file: string) {\n        if (file in this.faDataTypeCache) {\n            return this.faDataTypeCache[file];\n        }\n        const fileExt = nodePath.extname(file).toLowerCase();\n        if (fileExt !== \".fad\") {\n            return null;\n        }\n        try {\n            const result = await Files.read(file, {\n                isDataPath: false,\n            });\n            const json = JSON.parse(result);\n            this.faDataTypeCache[file] = json[\"type\"];\n        } catch (e) {\n            this.faDataTypeCache[file] = null;\n        }\n        return this.faDataTypeCache[file];\n    },\n    async filterFadType(files: FileItem[], types: string[]) {\n        const newFiles = [];\n        for (const file of files) {\n            const fileExt = nodePath.extname(file.path).toLowerCase();\n            if (fileExt !== \".fad\") {\n                continue;\n            }\n            const fileType = await this.getFaDataTypeCached(file.path);\n            if (!fileType) {\n                continue;\n            }\n            if (types.includes(fileType)) {\n                newFiles.push(file);\n            }\n        }\n        return newFiles;\n    },\n    async openQueue(filePath: string) {\n        this.filePath = filePath;\n        await this.openFileEditor();\n    },\n    async openFileEditor() {\n        return new Promise<any>(async (resolve) => {\n            const run = async () => {\n                if (!this.isReady) {\n                    setTimeout(run, 100);\n                    return;\n                }\n                if (!this.filePath) {\n                    Log.info(\n                        \"ManagerEditor.openFileEditor.Empty\",\n                        this.filePath,\n                    );\n                    return;\n                }\n                if (\n                    !(await Files.exists(this.filePath, { isDataPath: false }))\n                ) {\n                    Log.info(\n                        \"ManagerEditor.openFileEditor.NotFound\",\n                        this.filePath,\n                    );\n                    return;\n                }\n                const fileExt = nodePath.extname(this.filePath).toLowerCase();\n                const file: FileItem = {\n                    name: nodePath.basename(this.filePath),\n                    isDirectory: false,\n                    isFile: true,\n                    path: this.filePath,\n                    fileExt: fileExt.replace(\".\", \"\"),\n                };\n                const actions = await Manager.matchActionSimple({\n                    currentFiles: [file],\n                    activeWindow: null,\n                } as SearchQuery);\n                // Log.info('ManagerEditor.openFileEditor.Actions', JSON.stringify(actions, null, 2))\n                if (actions.length > 0) {\n                    Manager.openAction(\n                        JSON.parse(JSON.stringify(actions[0])),\n                    ).then();\n                } else {\n                    AppsMain.toast(t(\"editor.noPluginForFile\"), {\n                        status: \"error\",\n                    });\n                }\n                resolve(undefined);\n            };\n            run().then();\n        });\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/hotkey/handle.ts",
    "content": "import { ManagerPluginEvent } from \"../plugin/event\";\nimport { ManagerConfig } from \"../config/config\";\nimport ConfigMain from \"../../config/main\";\n\nexport const ManagerHotkeyHandle = {\n    async mainTrigger() {\n        if (await ManagerPluginEvent.isMainWindowShown(null, null)) {\n            if (await ManagerPluginEvent.isMainWindowFocused(null, null)) {\n                await ManagerPluginEvent.hideMainWindow(null, null);\n            } else {\n                await ManagerPluginEvent.showMainWindow(null, null);\n            }\n        } else {\n            await ManagerPluginEvent.showMainWindow(null, null);\n        }\n    },\n    async fastPanelTrigger() {\n        if (!(await ConfigMain.get(\"fastPanelEnable\", true))) {\n            return;\n        }\n        if (await ManagerPluginEvent.isFastPanelWindowShown(null, null)) {\n            await ManagerPluginEvent.hideFastPanelWindow(null, null);\n        } else {\n            await ManagerPluginEvent.showFastPanelWindow(null, null);\n        }\n    },\n    async launch(index: string) {\n        const i = parseInt(index);\n        const launches = await ManagerConfig.listLaunch();\n        if (i < launches.length) {\n            await ManagerPluginEvent.redirect(null, {\n                keywordsOrAction: launches[i].keyword,\n            });\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/hotkey/index.ts",
    "content": "import { uIOhook, UiohookKey } from \"uiohook-napi\";\nimport { ManagerConfig } from \"../config/config\";\nimport { ManagerHotkeyHandle } from \"./handle\";\nimport { HotkeyMouseButtonEnum } from \"../../keys/type\";\nimport { Events } from \"../../event/main\";\nimport { KeysMain } from \"../../keys/main\";\nimport { globalShortcut } from \"electron\";\n\ntype HotkeyKeyItem = {\n    name: string;\n\n    keycode: any;\n    altKey: boolean;\n    ctrlKey: boolean;\n    metaKey: boolean;\n    shiftKey: boolean;\n    times: number;\n\n    expireTimer?: number;\n};\n\ntype HotkeyKeySimpleItem = {\n    name: string;\n\n    type: \"Ctrl\" | \"Alt\" | \"Meta\";\n    times: number;\n};\n\ntype HotkeyMouseItem = {\n    name: string;\n\n    button: HotkeyMouseButtonEnum;\n    type: \"click\" | \"longPress\";\n    clickTimes?: number;\n\n    expireTime?: number;\n    expireCount?: number;\n};\n\nconst keyToKeyCode = (key: string) => {\n    if (key in UiohookKey) {\n        return UiohookKey[key];\n    }\n    return 0;\n};\n\nconst keyCodeToKey = (keyCode: number) => {\n    for (const key in UiohookKey) {\n        if (UiohookKey[key] === keyCode) {\n            return key;\n        }\n    }\n    return \"\";\n};\n\nexport const ManagerHotkey = {\n    isGrab: false,\n    keyMultiDelayTime: 500,\n    keyConfigs: [\n        // {\n        //     name: 'mainTrigger',\n        //     keycode: UiohookKey.Space,\n        //     altKey: false,\n        //     ctrlKey: false,\n        //     metaKey: true,\n        //     shiftKey: false,\n        //     times: 1,\n        // },\n    ] as HotkeyKeyItem[],\n    keySimpleConfigs: [\n        // {\n        //     name: 'fastPanelTrigger',\n        //     type: 'Ctrl',\n        //     times: 2,\n        // }\n    ] as HotkeyKeySimpleItem[],\n    mouseLongPressTime: 500,\n    mouseConfigs: [\n        // {\n        //     name: 'fastPanelTrigger',\n        //     type: 'click',\n        //     button: HotkeyButtonEnum.RIGHT,\n        //     clickTimes: 1,\n        // },\n        // {\n        //     name: 'fastPanelTrigger2',\n        //     type: 'longPress',\n        //     button: HotkeyButtonEnum.RIGHT,\n        // }\n    ] as HotkeyMouseItem[],\n\n    _keySimple: {\n        // Ctrl: null as null | 'down' | 'up',\n        // Alt: null as null | 'down' | 'up',\n        // Meta: null as null | 'down' | 'up',\n        down: null as null | \"Ctrl\" | \"Alt\" | \"Meta\",\n        key: null as null | \"Ctrl\" | \"Alt\" | \"Meta\",\n        expire: 0,\n        times: 0,\n    },\n\n    init() {\n        uIOhook.on(\"keydown\", (e) => {\n            if (this.isGrab) {\n                const data = {\n                    type: \"keydown\",\n                    key: keyCodeToKey(e.keycode),\n                    altKey: e.altKey,\n                    ctrlKey: e.ctrlKey,\n                    metaKey: e.metaKey,\n                    shiftKey: e.shiftKey,\n                };\n                Events.broadcast(\"HotkeyWatch\", data);\n                return;\n            }\n            // console.log('ManagerHotkey.keydown', e, this.keyConfigs)\n            // keyConfigs start\n            for (const item of this.keyConfigs) {\n                if (\n                    item.keycode !== e.keycode ||\n                    item.altKey !== e.altKey ||\n                    item.ctrlKey !== e.ctrlKey ||\n                    item.metaKey !== e.metaKey ||\n                    item.shiftKey !== e.shiftKey\n                ) {\n                    continue;\n                }\n                if (!item.times || item.times <= 1) {\n                    this.fire(item.name);\n                    return;\n                }\n                const now = Date.now();\n                if (!item.expireTime || now > item.expireTime) {\n                    item.expireTime = now + this.keyMultiDelayTime;\n                    item.expireCount = 1;\n                } else {\n                    item.expireCount++;\n                    if (item.expireCount >= item.times) {\n                        this.fire(item.name);\n                        item.expireTime = 0;\n                        item.expireCount = 0;\n                        return;\n                    }\n                }\n            }\n            // keyConfigs end\n            // keySimpleConfigs start\n            if (\n                e.keycode === UiohookKey.Ctrl &&\n                !e.altKey &&\n                e.ctrlKey &&\n                !e.metaKey &&\n                !e.shiftKey\n            ) {\n                this._keySimple.down = \"Ctrl\";\n            } else if (\n                e.keycode === UiohookKey.Alt &&\n                e.altKey &&\n                !e.ctrlKey &&\n                !e.metaKey &&\n                !e.shiftKey\n            ) {\n                this._keySimple.down = \"Alt\";\n            } else if (\n                e.keycode === UiohookKey.Meta &&\n                !e.altKey &&\n                !e.ctrlKey &&\n                e.metaKey &&\n                !e.shiftKey\n            ) {\n                this._keySimple.down = \"Meta\";\n            } else {\n                this._keySimple.down = null;\n            }\n            // keySimpleConfigs end\n        });\n        const keySimpleUp = (key: \"Ctrl\" | \"Alt\" | \"Meta\") => {\n            // console.log('keySimpleUp', key, JSON.stringify(this.keySimpleConfigs))\n            const now = Date.now();\n            if (this._keySimple.expire > now && key === this._keySimple.key) {\n                this._keySimple.times++;\n            } else {\n                this._keySimple.times = 1;\n                this._keySimple.key = key;\n            }\n            this._keySimple.expire = now + this.keyMultiDelayTime;\n            this.keySimpleConfigs\n                .filter(\n                    (o) => o.type === key && o.times <= this._keySimple.times,\n                )\n                .forEach((o) => this.fire(o.name));\n        };\n        uIOhook.on(\"keyup\", (e) => {\n            if (\n                e.keycode === UiohookKey.Ctrl &&\n                !e.altKey &&\n                !e.ctrlKey &&\n                !e.metaKey &&\n                !e.shiftKey &&\n                this._keySimple.down === \"Ctrl\"\n            ) {\n                keySimpleUp(\"Ctrl\");\n            } else if (\n                e.keycode === UiohookKey.Alt &&\n                !e.altKey &&\n                !e.ctrlKey &&\n                !e.metaKey &&\n                !e.shiftKey &&\n                this._keySimple.down === \"Alt\"\n            ) {\n                keySimpleUp(\"Alt\");\n            } else if (\n                e.keycode === UiohookKey.Meta &&\n                !e.altKey &&\n                !e.ctrlKey &&\n                !e.metaKey &&\n                !e.shiftKey &&\n                this._keySimple.down === \"Meta\"\n            ) {\n                keySimpleUp(\"Meta\");\n            }\n        });\n        // uIOhook.on('mousedown', (e) => {\n        //     // console.log('ManagerHotkey.mousedown', e)\n        //     for (const item of this.mouseConfigs) {\n        //         if (item.button !== e.button) {\n        //             continue\n        //         }\n        //         if (item.type === 'click') {\n        //             if (!item.clickTimes || item.clickTimes <= 1) {\n        //                 this.fire(item.name)\n        //             } else if (item.clickTimes === e.clicks) {\n        //                 this.fire(item.name)\n        //             }\n        //         } else if (item.type === 'longPress') {\n        //             item.expireTimer = setTimeout(() => {\n        //                 this.fire(item.name)\n        //                 item.expireTimer = 0\n        //             }, this.mouseLongPressTime)\n        //         }\n        //     }\n        // })\n        // uIOhook.on('mouseup', (e) => {\n        //     // console.log('ManagerHotkey.mouseup', e)\n        //     for (const item of this.mouseConfigs) {\n        //         if (item.button === HotkeyMouseButtonEnum.LEFT && e.button !== 1) {\n        //             continue\n        //         }\n        //         if (item.button === HotkeyMouseButtonEnum.RIGHT && e.button !== 2) {\n        //             continue\n        //         }\n        //         if (item.type === 'longPress') {\n        //             if (item.expireTimer) {\n        //                 clearTimeout(item.expireTimer)\n        //                 item.expireTimer = 0\n        //             }\n        //         }\n        //     }\n        // })\n        uIOhook.start();\n        this.configInit().then();\n    },\n\n    destroy() {\n        uIOhook.stop();\n    },\n\n    async register() {\n        // console.log('ManagerHotkey.register', this.keyConfigs)\n        for (const keyConfig of this.keyConfigs) {\n            const accelerator = [];\n            if (keyConfig.ctrlKey) {\n                accelerator.push(\"Control\");\n            }\n            if (keyConfig.metaKey) {\n                accelerator.push(\"Meta\");\n            }\n            if (keyConfig.altKey) {\n                accelerator.push(\"Alt\");\n            }\n            if (keyConfig.shiftKey) {\n                accelerator.push(\"Shift\");\n            }\n            accelerator.push(keyCodeToKey(keyConfig.keycode));\n            globalShortcut.register(accelerator.join(\"+\"), () => {\n                this.fire(keyConfig.name);\n            });\n        }\n        this.keyConfigs = this.keyConfigs.filter(\n            (item) => item.times && item.times > 1,\n        );\n    },\n\n    async configInit() {\n        this.keyConfigs = [];\n\n        const config = await ManagerConfig.get();\n        for (const k of [\"mainTrigger\"]) {\n            if (config[k]) {\n                this.keyConfigs.push({\n                    name: k,\n                    keycode: keyToKeyCode(config[k].key),\n                    altKey: config[k].altKey,\n                    ctrlKey: config[k].ctrlKey,\n                    metaKey: config[k].metaKey,\n                    shiftKey: config[k].shiftKey,\n                    times: config[k].times,\n                });\n            }\n        }\n        this.keySimpleConfigs = [];\n        if (config.fastPanelTrigger) {\n            this.keySimpleConfigs.push({\n                name: \"fastPanelTrigger\",\n                type: config.fastPanelTrigger.type,\n                times: config.fastPanelTrigger.times || 1,\n            });\n        }\n\n        const launches = await ManagerConfig.listLaunch();\n        launches.forEach((launch, launchIndex) => {\n            if (launch.hotkey && launch.keyword) {\n                this.keyConfigs.push({\n                    name: `launch:${launchIndex}`,\n                    keycode: keyToKeyCode(launch.hotkey.key),\n                    altKey: launch.hotkey.altKey,\n                    ctrlKey: launch.hotkey.ctrlKey,\n                    metaKey: launch.hotkey.metaKey,\n                    shiftKey: launch.hotkey.shiftKey,\n                    times: launch.hotkey.times,\n                });\n            }\n        });\n\n        // this.mouseConfigs = []\n        // if (config.fastPanelTriggerButton) {\n        //     this.mouseConfigs.push({\n        //         name: 'fastPanelTrigger',\n        //         type: config.fastPanelTriggerButton.type,\n        //         button: config.fastPanelTriggerButton.button,\n        //         clickTimes: config.fastPanelTriggerButton.clickTimes,\n        //     })\n        // }\n\n        KeysMain.register();\n    },\n\n    async watch() {\n        this.isGrab = true;\n    },\n    async unwatch() {\n        this.isGrab = false;\n    },\n\n    eventListeners: {},\n    fire(eventName: string, ...args: any[]) {\n        // console.log('ManagerHotkey.fire', eventName, args)\n        let eventParam = \"\";\n        if (eventName.includes(\":\")) {\n            const pcs = eventName.split(\":\");\n            if (pcs.length > 1) {\n                eventName = pcs[0];\n                eventParam = pcs[1];\n            }\n        }\n        if (eventName in ManagerHotkeyHandle) {\n            ManagerHotkeyHandle[eventName](eventParam);\n        }\n        if (!this.eventListeners[eventName]) {\n            return;\n        }\n        this.eventListeners[eventName].forEach((cb) => cb(...args));\n    },\n    on(eventName: string, callback: Function) {\n        if (!this.eventListeners[eventName]) {\n            this.eventListeners[eventName] = [];\n        }\n        this.eventListeners[eventName].push(callback);\n    },\n    off(eventName: string, callback: Function) {\n        if (!this.eventListeners[eventName]) {\n            return;\n        }\n        this.eventListeners[eventName] = this.eventListeners[eventName].filter(\n            (cb) => cb !== callback,\n        );\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/hotkey/simulate.ts",
    "content": "import { uIOhook, UiohookKey } from \"uiohook-napi\";\n\nexport const KeyboardKey = {\n    ...UiohookKey,\n};\n\nexport const ManagerHotkeySimulate = {\n    toCode(key: string) {\n        return key in KeyboardKey ? KeyboardKey[key] : key;\n    },\n    keyTap(key: number, modifiers?: number[]) {\n        uIOhook.keyTap(key, modifiers);\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/lib/cache.ts",
    "content": "import { Files } from \"../../file/main\";\nimport { Log } from \"../../log/main\";\n\nexport const ManagerFileCacheUtil = {\n    async get(name: string, defaultValue: any = null) {\n        const content = await Files.read(`cache/${name}.json`, {\n            isDataPath: true,\n        });\n        if (content) {\n            let json = null;\n            try {\n                json = JSON.parse(content);\n            } catch (e) {\n                Log.error(\"Plugin.App.Error\", e);\n            }\n            if (!json || !(\"expire\" in json) || !(\"value\" in json)) {\n                await Files.deletes(`cache/${name}.json`, { isDataPath: true });\n                return defaultValue;\n            }\n            if (json.expire > 0 && json.expire < Date.now()) {\n                await Files.deletes(`cache/${name}.json`, { isDataPath: true });\n                return defaultValue;\n            }\n            return json.value;\n        }\n        return defaultValue;\n    },\n    async getIgnoreExpire(\n        name: string,\n        defaultValue: any = null,\n    ): Promise<{\n        isCache: boolean;\n        value: any;\n        expire: number;\n    }> {\n        const content = await Files.read(`cache/${name}.json`, {\n            isDataPath: true,\n        });\n        if (content) {\n            let json = null;\n            try {\n                json = JSON.parse(content);\n            } catch (e) {\n                Log.error(\"Plugin.App.Error\", e);\n            }\n            if (!json || !(\"value\" in json)) {\n                await Files.deletes(`cache/${name}.json`, { isDataPath: true });\n                return {\n                    isCache: false,\n                    value: defaultValue,\n                    expire: 0,\n                };\n            }\n            return {\n                isCache: true,\n                value: json.value,\n                expire: json.expire,\n            };\n        }\n        return {\n            isCache: false,\n            value: defaultValue,\n            expire: 0,\n        };\n    },\n    async set(name: string, value: any, expire: number = 0) {\n        const json = {\n            expire: expire > 0 ? Date.now() + expire : 0,\n            value: value,\n        };\n        await Files.write(`cache/${name}.json`, JSON.stringify(json), {\n            isDataPath: true,\n        });\n    },\n    async forget(name: string) {\n        await Files.deletes(`cache/${name}.json`, { isDataPath: true });\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/lib/hooks.ts",
    "content": "import { BrowserView, BrowserWindow } from \"electron\";\nimport { AppsMain } from \"../../app/main\";\nimport { PluginRecord } from \"../../../../src/types/Manager\";\n\ntype PluginHookType =\n    | never\n    | \"PluginReady\"\n    | \"PluginExit\"\n    | \"PluginEvent\"\n    | \"MoreMenuClick\"\n    | \"DetachOperateClick\"\n    | \"SubInputChange\"\n    | \"ScreenCapture\"\n    | \"Hotkey\";\n\ntype HookType =\n    | never\n    | \"Show\"\n    | \"Hide\"\n    | \"SetSubInput\"\n    | \"RemoveSubInput\"\n    | \"SetSubInputValue\"\n    | \"PluginInit\"\n    | \"PluginInitReady\"\n    | \"PluginExit\"\n    | \"PluginAlreadyOpened\"\n    | \"PluginDetached\"\n    | \"PluginState\"\n    | \"PluginCodeInit\"\n    | \"PluginCodeData\"\n    | \"PluginCodeSetting\"\n    | \"PluginCodeExit\"\n    | \"DetachSet\"\n    | \"Maximize\"\n    | \"Unmaximize\"\n    | \"EnterFullScreen\"\n    | \"LeaveFullScreen\"\n    | \"DetachWindowClosed\";\n\nexport const executePluginHooks = async (\n    view: BrowserView,\n    hook: PluginHookType,\n    data?: any,\n) => {\n    const evalJs = `\n    if(window.focusany && window.focusany.hooks && typeof window.focusany.hooks.on${hook} === 'function' ) {\n        try {\n            window.focusany.hooks.on${hook}(${JSON.stringify(data)});\n        } catch(e) {\n            console.log('executePluginHooks.on${hook}.error', e);\n        }\n    }`;\n    return view.webContents?.executeJavaScript(evalJs);\n};\n\nexport const executeHooks = async (\n    win: BrowserWindow,\n    hook: HookType,\n    data?: any,\n) => {\n    const evalJs = `\n    if(window.__page && window.__page.hooks && typeof window.__page.hooks.on${hook} === 'function' ) {\n        try {\n            window.__page.hooks.on${hook}(${JSON.stringify(data)});\n        } catch(e) {\n            console.log('executeHooks.on${hook}.error', e);\n        }\n    }`;\n    return win.webContents?.executeJavaScript(evalJs);\n};\n\nexport const executeDarkMode = async (\n    view: BrowserWindow | BrowserView,\n    data: {\n        plugin: PluginRecord;\n        isSystem: boolean;\n    },\n) => {\n    // console.log('executeDarkMode', data.plugin.setting);\n    if (\n        (await AppsMain.shouldDarkMode()) &&\n        (data.plugin.setting?.darkModeSupport || data.isSystem)\n    ) {\n        // body and html\n        view.webContents.executeJavaScript(`\n        document.body.setAttribute('data-theme', 'dark');\n        document.documentElement.setAttribute('data-theme', 'dark');\n        `);\n        if (data.isSystem) {\n            view.webContents.executeJavaScript(\n                `document.body.setAttribute('arco-theme', 'dark');`,\n            );\n        }\n    }\n};\n"
  },
  {
    "path": "electron/mapi/manager/main.ts",
    "content": "import { BrowserWindow, ipcMain } from \"electron\";\nimport {\n    ActionRecord,\n    ActionTypeEnum,\n    FilePluginRecord,\n    LaunchRecord,\n    PluginEnv,\n} from \"../../../src/types/Manager\";\nimport { t } from \"../../config/lang\";\nimport { Permissions } from \"../../lib/permission\";\nimport { Page } from \"../../page\";\nimport { AppsMain } from \"../app/main\";\nimport { AppRuntime } from \"../env\";\nimport ProtocolMain from \"../protocol/main\";\nimport { ManagerAutomation } from \"./automation\";\nimport { ManagerBackend } from \"./backend\";\nimport { ManagerClipboard } from \"./clipboard\";\nimport { ManagerConfig } from \"./config/config\";\nimport { ManagerEditor } from \"./editor\";\nimport { ManagerHotkey } from \"./hotkey\";\nimport { Manager } from \"./manager\";\nimport { ManagerPlugin } from \"./plugin\";\nimport { ManagerPluginEvent } from \"./plugin/event\";\nimport { PluginHttp } from \"./plugin/http\";\nimport { PluginHttpMCP } from \"./plugin/httpMCP\";\nimport { PluginLog } from \"./plugin/log\";\nimport { ManagerSystem } from \"./system\";\nimport { ManagerSystemPluginFile } from \"./system/plugin/file\";\nimport { ManagerPluginStore } from \"./system/plugin/store/index\";\nimport { PluginContext, SearchQuery } from \"./type\";\nimport { ManagerWindow } from \"./window\";\n\nconst init = () => {\n    ManagerClipboard.init().then();\n    ManagerEditor.init().then();\n    PluginHttp.init().then();\n};\n\nconst ready = () => {\n    Permissions.checkAccessibilityAccess().then((enable) => {\n        if (enable) {\n            ManagerHotkey.init();\n        } else {\n            Page.open(\"setup\").then();\n        }\n    });\n    Permissions.checkScreenCaptureAccess().then((enable) => {\n        if (enable) {\n            ManagerAutomation.init();\n        } else {\n            Page.open(\"setup\").then();\n        }\n    });\n    ManagerEditor.ready().then();\n};\n\nconst destroy = () => {\n    ManagerClipboard.monitorStop();\n    ManagerHotkey.destroy();\n};\n\nipcMain.handle(\"manager:getConfig\", async (event) => {\n    return await ManagerConfig.get();\n});\n\nipcMain.handle(\"manager:setConfig\", async (event, config) => {\n    return await ManagerConfig.save(config);\n});\n\nipcMain.handle(\"manager:getMcpServer\", async (event) => {\n    return await PluginHttp.getMcpServer();\n});\n\nipcMain.handle(\"manager:getMcpInfo\", async (event) => {\n    const toolsResult = await PluginHttpMCP[\"tools/list\"]({});\n    return {\n        tools: toolsResult.tools,\n    };\n});\n\nipcMain.handle(\"manager:isShown\", async (event) => {\n    return await ManagerPluginEvent.isMainWindowShown(null, null);\n});\n\nipcMain.handle(\"manager:show\", async (event) => {\n    return await ManagerPluginEvent.showMainWindow(null, null);\n});\n\nipcMain.handle(\"manager:hide\", async (event) => {\n    return await ManagerPluginEvent.hideMainWindow(null, null);\n});\n\nipcMain.handle(\"manager:getClipboardContent\", async (event) => {\n    return await ManagerClipboard.getClipboardContent();\n});\n\nipcMain.handle(\"manager:getClipboardChangeTime\", async (event) => {\n    return ManagerClipboard.lastChangeTimestamp;\n});\n\nipcMain.handle(\"manager:getSelectedContent\", async (event) => {\n    return Manager.selectedContent;\n});\n\nipcMain.handle(\"manager:listPlugin\", async (event, option?: {}) => {\n    return await Manager.listPlugin();\n});\n\nipcMain.handle(\n    \"manager:installPlugin\",\n    async (event, fileOrPath: string, option?: {}) => {\n        return await ManagerPlugin.installFromFileOrDir(fileOrPath);\n    },\n);\n\nipcMain.handle(\n    \"manager:refreshInstallPlugin\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerPlugin.refreshInstall(pluginName);\n    },\n);\n\nipcMain.handle(\n    \"manager:uninstallPlugin\",\n    async (event, pluginName: string, option?: {}) => {\n        // console.log('manager:uninstallPlugin', pluginName)\n        return await ManagerPlugin.uninstall(pluginName);\n    },\n);\n\nipcMain.handle(\n    \"manager:getPluginInstalledVersion\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerPlugin.getPluginInstalledVersion(pluginName);\n    },\n);\n\nipcMain.handle(\n    \"manager:listDisabledActionMatch\",\n    async (event, option?: {}) => {\n        return await ManagerConfig.listDisabledActionMatch();\n    },\n);\n\nipcMain.handle(\n    \"manager:toggleDisabledActionMatch\",\n    async (\n        event,\n        pluginName: string,\n        actionName: string,\n        matchName: string,\n        option?: {},\n    ) => {\n        return await ManagerConfig.toggleDisabledActionMatch(\n            pluginName,\n            actionName,\n            matchName,\n        );\n    },\n);\n\nipcMain.handle(\"manager:listPinAction\", async (event, option?: {}) => {\n    return await ManagerConfig.listPinAction();\n});\n\nipcMain.handle(\n    \"manager:togglePinAction\",\n    async (event, pluginName: string, actionName: string, option?: {}) => {\n        return await ManagerConfig.togglePinAction(pluginName, actionName);\n    },\n);\n\nipcMain.handle(\n    \"manager:showLog\",\n    async (event, pluginName: string, option?: {}) => {\n        await ManagerPluginEvent.logShow(\n            {\n                _plugin: {\n                    name: pluginName,\n                },\n            } as any,\n            {},\n        );\n    },\n);\n\nipcMain.handle(\"manager:clearCache\", async (event, option?: {}) => {\n    await ManagerConfig.clearCache();\n    await ManagerSystem.clearCache();\n    await ManagerPlugin.clearCache();\n});\n\nipcMain.handle(\"manager:hotKeyWatch\", async (event, option?: {}) => {\n    await ManagerHotkey.watch();\n});\n\nipcMain.handle(\"manager:hotKeyUnwatch\", async (event, option?: {}) => {\n    await ManagerHotkey.unwatch();\n});\n\nipcMain.handle(\"manager::listAction\", async (event) => {\n    return Manager.listAction();\n});\n\nconst mergeViewActionRuntime = async (actions: ActionRecord[]) => {\n    for (const a of actions) {\n        const plugin = await Manager.getPlugin(a.pluginName);\n        const { nodeIntegration, preloadBase, mainView } =\n            await ManagerPlugin.getInfo(plugin);\n        a.runtime.view = {\n            nodeIntegration,\n            preloadBase,\n            mainView,\n            showViewDevTools: false,\n            heightView: 100,\n        };\n        // console.log('mergeViewActionRuntime', plugin.development)\n        if (\n            plugin.development &&\n            plugin.development.env === PluginEnv.DEV &&\n            plugin.development.showViewDevTools\n        ) {\n            a.runtime.view.showViewDevTools = true;\n        }\n        if (plugin.setting && plugin.setting.heightView) {\n            a.runtime.view.heightView = plugin.setting.heightView;\n        }\n        for (const k of [\"preloadBase\", \"mainView\"]) {\n            if (\n                a.runtime.view[k] &&\n                !a.runtime.view[k].startsWith(\"file://\") &&\n                !a.runtime.view[k].startsWith(\"http://\")\n            ) {\n                a.runtime.view[k] = \"file://\" + a.runtime.view[k];\n            }\n        }\n    }\n};\n\nipcMain.handle(\n    \"manager:searchFastPanelAction\",\n    async (event, query: SearchQuery, option?: {}) => {\n        query = Object.assign(\n            {\n                keywords: \"\",\n                currentFiles: [],\n                currentImage: \"\",\n                currentText: \"\",\n                activeWindow: Manager.activeWindow,\n            },\n            query,\n        );\n\n        // console.log('manager:searchFastPanelAction', query)\n\n        const request = Manager.createSearchRequest(query);\n        const result = {\n            id: request.id,\n            matchActions: [] as ActionRecord[],\n            viewActions: [] as ActionRecord[],\n        };\n\n        const actions = await Manager.listAction(request);\n        const actionFullNameMap = new Map();\n        for (const a of actions) {\n            actionFullNameMap.set(a.fullName, a);\n        }\n        const uniqueRemover = new Set<string>();\n        result.matchActions = [\n            ...(await Manager.matchActions(uniqueRemover, actions, query)),\n            ...(await Manager.searchActions(uniqueRemover, actions, query)),\n            ...(await Manager.pinActions(\n                uniqueRemover,\n                actionFullNameMap,\n                query,\n            )),\n            ...(await Manager.historyActions(\n                uniqueRemover,\n                actionFullNameMap,\n                query,\n            )),\n        ];\n        result.viewActions = result.matchActions.filter(\n            (a) => a.type === ActionTypeEnum.VIEW && a.data?.showFastPanel,\n        );\n        result.matchActions = result.matchActions.filter(\n            (a) => a.type !== ActionTypeEnum.VIEW,\n        );\n\n        await mergeViewActionRuntime(result.viewActions);\n\n        return result;\n    },\n);\n\nipcMain.handle(\n    \"manager:searchAction\",\n    async (event, query: SearchQuery, option?: {}) => {\n        query = Object.assign(\n            {\n                keywords: \"\",\n                currentFiles: [],\n                currentImage: \"\",\n                currentText: \"\",\n                activeWindow: Manager.activeWindow,\n            },\n            query,\n        );\n        // console.log('manager:searchAction', query)\n\n        const request = Manager.createSearchRequest(query);\n        const result = {\n            id: request.id,\n            detachWindowActions: [],\n            searchActions: [],\n            matchActions: [],\n            viewActions: [],\n            historyActions: [],\n            pinActions: [],\n        };\n\n        // 所有已知的动作\n        const actions = await Manager.listAction(request);\n        const actionFullNameMap = new Map();\n        for (const a of actions) {\n            actionFullNameMap.set(a.fullName, a);\n        }\n        // Files.write('actions.json', JSON.stringify(actions))\n\n        const uniqueRemover = new Set<string>();\n        if (!query.keywords) {\n            result.detachWindowActions = await Manager.detachWindowActions(\n                uniqueRemover,\n                actionFullNameMap,\n            );\n        }\n        result.searchActions = await Manager.searchActions(\n            uniqueRemover,\n            actions,\n            query,\n        );\n        result.matchActions = await Manager.matchActions(\n            uniqueRemover,\n            actions,\n            query,\n        );\n        result.viewActions = [\n            ...result.searchActions.filter(\n                (a) => a.type === ActionTypeEnum.VIEW && a.data?.showMainPanel,\n            ),\n            ...result.matchActions.filter(\n                (a) => a.type === ActionTypeEnum.VIEW && a.data?.showMainPanel,\n            ),\n        ];\n        result.searchActions = result.searchActions.filter(\n            (a) => a.type !== ActionTypeEnum.VIEW,\n        );\n        result.matchActions = result.matchActions.filter(\n            (a) => a.type !== ActionTypeEnum.VIEW,\n        );\n        if (!query.keywords) {\n            result.historyActions = await Manager.historyActions(\n                uniqueRemover,\n                actionFullNameMap,\n                query,\n            );\n            result.pinActions = await Manager.pinActions(\n                new Set(),\n                actionFullNameMap,\n                query,\n            );\n        }\n\n        const pinedSet = await ManagerConfig.getPinedActionSet();\n        result.searchActions.forEach((a) => {\n            a.runtime.isPined = pinedSet.has(`${a.pluginName}/${a.name}`);\n        });\n        result.matchActions.forEach((a) => {\n            a.runtime.isPined = pinedSet.has(`${a.pluginName}/${a.name}`);\n        });\n        result.historyActions.forEach((a) => {\n            a.runtime.isPined = pinedSet.has(`${a.pluginName}/${a.name}`);\n        });\n        result.pinActions.forEach((a) => {\n            a.runtime.isPined = true;\n        });\n\n        await mergeViewActionRuntime(result.viewActions);\n\n        return result;\n    },\n);\n\nipcMain.handle(\n    \"manager:listDetachWindowActions\",\n    async (event, option?: {}) => {\n        const actions = await Manager.listAction();\n        const actionFullNameMap = new Map();\n        for (const a of actions) {\n            actionFullNameMap.set(a.fullName, a);\n        }\n        const uniqueRemover = new Set<string>();\n        const result = await Manager.detachWindowActions(\n            uniqueRemover,\n            actionFullNameMap,\n        );\n        await mergeViewActionRuntime(result);\n        return result;\n    },\n);\n\nipcMain.handle(\n    \"manager:subInputChange\",\n    async (event, keywords: string, option?: {}) => {\n        const senderWindow = BrowserWindow.fromWebContents(event.sender);\n        await ManagerWindow.subInputChange(senderWindow, keywords);\n    },\n);\n\nipcMain.handle(\n    \"manager:openPlugin\",\n    async (event, pluginName: string, option?: {}) => {\n        await Manager.openPlugin(pluginName);\n    },\n);\n\nipcMain.handle(\"manager:openAction\", async (event, action: ActionRecord) => {\n    await Manager.openAction(action);\n});\n\nipcMain.handle(\"manager:openActionCode\", async (event, id: string) => {\n    await ManagerWindow.actionCodeExecute(id, null);\n});\n\nipcMain.handle(\"manager:searchActionCode\", async (event, keywords: string) => {\n    await ManagerWindow.actionCodeExecute(null, keywords);\n});\n\nipcMain.handle(\n    \"manager:openActionWindow\",\n    async (event, type: \"open\" | \"close\", action: ActionRecord) => {\n        await Manager.openActionWindow(type, action);\n    },\n);\n\nipcMain.handle(\"manager:closeMainPlugin\", async (event, option?: {}) => {\n    await ManagerWindow.close(null);\n});\n\nipcMain.handle(\"manager:openMainPluginDevTools\", async (event, option?: {}) => {\n    await ManagerWindow.openMainPluginDevTools();\n});\n\nipcMain.handle(\"manager:openMainPluginLog\", async (event, option?: {}) => {\n    const view = ManagerWindow.getViewByWebContents(event.sender);\n    await ManagerPluginEvent.logShow(view, {});\n});\n\nipcMain.handle(\"manager:detachPlugin\", async (event, option) => {\n    await ManagerWindow.detach();\n});\n\nipcMain.handle(\n    \"manager:toggleDetachPluginAlwaysOnTop\",\n    async (event, alwaysOnTop: boolean, option?: {}) => {\n        const view = ManagerWindow.getViewByWebContents(event.sender);\n        return ManagerWindow.toggleDetachPluginAlwaysOnTop(\n            view,\n            alwaysOnTop,\n            option,\n        );\n    },\n);\n\nipcMain.handle(\n    \"manager:setDetachPluginZoom\",\n    async (event, zoom: number, option?: {}) => {\n        const view = ManagerWindow.getViewByWebContents(event.sender);\n        await ManagerWindow.setDetachPluginZoom(view, zoom, option);\n        await ManagerConfig.setPluginConfigItem(\n            view._plugin.name,\n            \"zoom\",\n            zoom,\n        );\n    },\n);\n\nipcMain.handle(\n    \"manager:firePluginMoreMenuClick\",\n    async (event, name: string, option?: {}) => {\n        const view = ManagerWindow.getViewByWebContents(event.sender);\n        await ManagerWindow.firePluginMoreMenuClick(view, name, option);\n    },\n);\n\nipcMain.handle(\n    \"manager:fireDetachOperateClick\",\n    async (event, name: string, option?: {}) => {\n        const view = ManagerWindow.getViewByWebContents(event.sender);\n        await ManagerWindow.fireDetachOperateClick(view, name, option);\n    },\n);\n\nipcMain.handle(\"manager:closeDetachPlugin\", async (event) => {\n    const view = ManagerWindow.getViewByWebContents(event.sender);\n    await ManagerWindow.closeDetachPlugin(view);\n});\n\nipcMain.handle(\n    \"manager:openDetachPluginDevTools\",\n    async (event, option?: {}) => {\n        const view = ManagerWindow.getViewByWebContents(event.sender);\n        await ManagerWindow.openDetachPluginDevTools(view);\n    },\n);\n\nipcMain.handle(\"manager:openDetachPluginLog\", async (event, option?: {}) => {\n    const view = ManagerWindow.getViewByWebContents(event.sender);\n    await ManagerPluginEvent.logShow(view, {});\n});\n\nipcMain.handle(\n    \"manager:setPluginAutoDetach\",\n    async (event, autoDetach: boolean, option?: {}) => {\n        const view = ManagerWindow.getViewByWebContents(event.sender);\n        await ManagerConfig.setPluginConfigItem(\n            view._plugin.name,\n            \"autoDetach\",\n            autoDetach,\n        );\n    },\n);\n\nipcMain.handle(\n    \"manager:getPluginConfig\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerConfig.getPluginConfig(pluginName);\n    },\n);\n\nipcMain.handle(\"manager:listFilePluginRecords\", async (event, option?: {}) => {\n    return await ManagerSystemPluginFile.list();\n});\n\nipcMain.handle(\n    \"manager:updateFilePluginRecords\",\n    async (event, records: FilePluginRecord[], option?: {}) => {\n        return await ManagerSystemPluginFile.update(records);\n    },\n);\n\nipcMain.handle(\"manager:listLaunchRecords\", async (event, option?: {}) => {\n    return await ManagerConfig.listLaunch();\n});\n\nipcMain.handle(\n    \"manager:updateLaunchRecords\",\n    async (event, records: LaunchRecord[], option?: {}) => {\n        return await ManagerConfig.updateLaunch(records);\n    },\n);\n\nipcMain.handle(\n    \"manager:storeInstall\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerPluginStore.install(pluginName, option);\n    },\n);\n\nProtocolMain.register(\"open\", async (params) => {\n    const pluginName = params.pluginName;\n    const pluginVersion = params.pluginVersion;\n    const autoInstall = params.autoInstall;\n    const plugin = await Manager.getPlugin(pluginName);\n    if (!plugin) {\n        if (autoInstall) {\n            const loading = AppsMain.loading(t(\"plugin.installing\"), {\n                percentAuto: true,\n            });\n            try {\n                await ManagerPluginStore.install(pluginName, {\n                    version: pluginVersion,\n                });\n            } catch (e) {\n                AppsMain.toast(t(\"plugin.installFailed\"), {\n                    status: \"error\",\n                });\n                return;\n            } finally {\n                loading.close();\n            }\n            AppsMain.toast(t(\"plugin.opening\"), {\n                status: \"success\",\n            });\n            await Manager.openPlugin(pluginName);\n        } else {\n            AppsMain.toast(t(\"plugin.notExist\", { name: pluginName }), {\n                status: \"error\",\n            });\n        }\n    } else {\n        AppsMain.toast(t(\"plugin.opening\"), {\n            status: \"success\",\n        });\n        await Manager.openPlugin(pluginName);\n    }\n});\n\nipcMain.handle(\n    \"manager:storePublish\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerPluginStore.publish(pluginName, option);\n    },\n);\n\nipcMain.handle(\n    \"manager:storePublishInfo\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerPluginStore.publishInfo(pluginName, option);\n    },\n);\n\nipcMain.handle(\n    \"manager:storeInstallingInfo\",\n    async (event, pluginName: string, option?: {}) => {\n        return await ManagerPluginStore.storeInstallingInfo(pluginName);\n    },\n);\n\nipcMain.handle(\n    \"manager:clipboardList\",\n    async (\n        event,\n        option?: {\n            limit?: number;\n        },\n    ) => {\n        option = Object.assign(\n            {\n                limit: -1,\n            },\n            option,\n        );\n        return await ManagerClipboard.list(option.limit);\n    },\n);\n\nipcMain.handle(\"manager:clipboardClear\", async (event, option?: {}) => {\n    return await ManagerClipboard.clear();\n});\n\nipcMain.handle(\n    \"manager:clipboardDelete\",\n    async (event, timestamp: number, option?: {}) => {\n        return await ManagerClipboard.delete(timestamp);\n    },\n);\n\nipcMain.handle(\"manager:historyClear\", async (event, option?: {}) => {\n    return await ManagerConfig.clearHistoryAction();\n});\n\nipcMain.handle(\n    \"manager:historyDelete\",\n    async (event, pluginName: string, actionName: string, option?: {}) => {\n        return await ManagerConfig.deleteHistoryAction(pluginName, actionName);\n    },\n);\n\nconst getViewByEvent = (event) => {\n    let view = ManagerWindow.getViewByWebContents(event.sender);\n    if (!view) {\n        try {\n            const userAgent = event.sender.getUserAgent();\n            const match = userAgent.match(/PluginAction\\/([^/]+)\\/([^/]+)$/);\n            if (match) {\n                const pluginName = match[1];\n                const actionName = match[2];\n                view = {\n                    _plugin: Manager.getPluginSync(pluginName),\n                } as PluginContext;\n            }\n        } catch (e) {}\n    }\n    return view;\n};\n\nipcMain.on(\"FocusAny.Event\", async (_event, payload: any) => {\n    const view = getViewByEvent(_event);\n    const { id, event, data } = payload;\n    // console.log('FocusAny.Event', {id, event, data, view})\n    const plugin = view._plugin;\n    const result = await ManagerBackend.run(plugin, \"event\", event, data, {\n        rejectIfError: true,\n    });\n    const resultEvent = `FocusAny.Event.${id}`;\n    view.webContents.send(resultEvent, result);\n});\n\nipcMain.on(\n    \"FocusAny.Plugin\",\n    (\n        event,\n        payload: {\n            type: string;\n            data: any;\n        },\n    ) => {\n        const view = getViewByEvent(event);\n        const { type, data } = payload;\n        ManagerPluginEvent[type](view, data)\n            .then((result) => {\n                event.returnValue = result;\n            })\n            .catch((e) => {\n                event.returnValue = e;\n                PluginLog.error(view._plugin.name, `ApiError.${type}`, {\n                    error: \"\" + e,\n                    data,\n                });\n            });\n    },\n);\n\nipcMain.handle(\n    \"FocusAny.Plugin.Async\",\n    async (\n        event,\n        payload: {\n            type: string;\n            data: any;\n        },\n    ) => {\n        const view = getViewByEvent(event);\n        const { type, data } = payload;\n        try {\n            return await ManagerPluginEvent[type](view, data);\n        } catch (e) {\n            PluginLog.error(view._plugin.name, `ApiError.${type}`, {\n                error: \"\" + e,\n                data,\n            });\n            throw e;\n        }\n    },\n);\n\nipcMain.on(\"SendTo\", (event, winId: number, type: string, ...args: any) => {\n    // console.log('SendTo', event.sender.getType(), event.sender.id, {winId, type, payload})\n    BrowserWindow.getAllWindows().forEach((w) => {\n        if (w === AppRuntime.fastPanelWindow) {\n            return;\n        }\n        if (w === AppRuntime.mainWindow) {\n            for (let v of w.getBrowserViews()) {\n                if (v.webContents.id === winId) {\n                    v.webContents.send(type, event.sender.id, ...args);\n                }\n            }\n        } else {\n            if (w.webContents.id === winId) {\n                w.webContents.send(type, event.sender.id, ...args);\n            }\n        }\n    });\n});\n\nexport default {\n    init,\n    ready,\n    destroy,\n};\n"
  },
  {
    "path": "electron/mapi/manager/manager.ts",
    "content": "import {\n    ActionMatchFile,\n    ActionMatchKey,\n    ActionMatchRegex,\n    ActionMatchText,\n    ActionMatchTypeEnum,\n    ActionMatchWindow,\n    ActionRecord,\n    ActionTypeEnum,\n    ActiveWindow,\n    PluginRecord,\n    SelectedContent,\n} from \"../../../src/types/Manager\";\nimport { ManagerSystem } from \"./system\";\nimport { ManagerPlugin } from \"./plugin\";\nimport { ManagerConfig } from \"./config/config\";\nimport { SearchQuery } from \"./type\";\nimport { PinyinUtil } from \"../../lib/pinyin-util\";\nimport { exec } from \"child_process\";\nimport { ManagerWindow } from \"./window\";\nimport { ManagerCode } from \"./code\";\nimport { ManagerBackend } from \"./backend\";\nimport { ReUtil, StrUtil } from \"../../lib/util\";\nimport { Events } from \"../event/main\";\nimport { ManagerEditor } from \"./editor\";\nimport { cloneDeep } from \"lodash\";\n\ntype SearchRequest = {\n    id: string;\n    query: SearchQuery;\n};\n\nlet plugins: PluginRecord[] = [];\n\nexport const Manager = {\n    selectedContent: null as SelectedContent | null,\n    activeWindow: null as ActiveWindow | null,\n    searchRequests: [] as SearchRequest[],\n    createSearchRequest(query: SearchQuery) {\n        const id = StrUtil.randomString(8);\n        if (this.searchRequests.length > 3) {\n            this.searchRequests.shift();\n        }\n        const request = {\n            id,\n            query,\n        };\n        this.searchRequests.push(request);\n        return request;\n    },\n    getSearchRequestQuery(id: string) {\n        for (const s of this.searchRequests) {\n            if (s.id === id) {\n                return s.query;\n            }\n        }\n        return null;\n    },\n    async openPlugin(pluginName: string) {\n        const plugin = await this.getPlugin(pluginName);\n        if (!plugin) {\n            throw \"PluginNotExists\";\n        }\n        if (!plugin.actions || !plugin.actions.length) {\n            throw \"PluginNoActions\";\n        }\n        for (const a of plugin.actions) {\n            if (a.type === ActionTypeEnum.WEB) {\n                await this.openAction(a);\n                return;\n            }\n        }\n    },\n    async openActionWindow(type: \"open\" | \"close\", action: ActionRecord) {\n        await ManagerWindow.detachWindowOperate(type, action);\n    },\n    async openAction(action: ActionRecord) {\n        const plugin = await Manager.getPlugin(action.pluginName);\n        if (!plugin) {\n            return;\n        }\n        if (!action.runtime) {\n            action.runtime = {\n                searchScore: 0,\n                searchTitleMatched: \"\",\n                match: null,\n            };\n        }\n        switch (action.type) {\n            case ActionTypeEnum.COMMAND:\n                exec(action.data.command);\n                break;\n            case ActionTypeEnum.WEB:\n                await ManagerWindow.open(plugin, action);\n                break;\n            case ActionTypeEnum.CODE:\n                await ManagerCode.execute(plugin, action);\n                break;\n            case ActionTypeEnum.BACKEND:\n                await ManagerBackend.runAction(plugin, action);\n                break;\n        }\n        if (action.trackHistory) {\n            await ManagerConfig.addHistoryAction(plugin, action);\n        }\n    },\n    async getPlugin(name: string) {\n        for (let p of await this.listPlugin()) {\n            if (p.name === name) {\n                return p;\n            }\n        }\n        return null;\n    },\n    getPluginSync(name: string) {\n        for (let p of plugins) {\n            if (p.name === name) {\n                return p;\n            }\n        }\n        return null;\n    },\n    async listPlugin() {\n        plugins = [\n            ...(await ManagerSystem.list()),\n            ...(await ManagerPlugin.list()),\n        ];\n        const customActions = await ManagerConfig.getCustomAction();\n        for (const p of plugins) {\n            if (!(p.name in customActions)) {\n                continue;\n            }\n            p.actions = p.actions.concat(customActions[p.name]);\n        }\n        return plugins;\n    },\n    async listAction(request?: SearchRequest) {\n        let actions: ActionRecord[] = [\n            ...(await ManagerSystem.listAction()),\n            ...(await ManagerPlugin.listAction()),\n        ];\n        const customActions = await ManagerConfig.getCustomAction();\n        for (const customAction of Object.values(customActions)) {\n            actions = actions.concat(customAction);\n        }\n        for (let a of actions) {\n            a.runtime = {\n                searchScore: 0,\n                searchTitleMatched: \"\",\n                match: null,\n                requestId: request ? request.id : null,\n            };\n        }\n        return actions;\n    },\n    async searchOneAction(\n        keywordsOrAction: string | string[],\n        query: SearchQuery,\n    ) {\n        const request = this.createSearchRequest(query);\n        query = Object.assign(\n            {\n                keywords: \"\",\n                currentFiles: [],\n                currentImage: \"\",\n                currentText: \"\",\n            },\n            query,\n        );\n        const actions = await this.listAction(request);\n        let action: ActionRecord = null;\n        if (typeof keywordsOrAction === \"string\") {\n            const uniqueRemover = new Set<string>();\n            const results = await this.searchActions(uniqueRemover, actions, {\n                ...query,\n                keywords: keywordsOrAction,\n            });\n            if (results.length > 0) {\n                action = results[0];\n            }\n        } else {\n            const fullName = keywordsOrAction.join(\"/\");\n            for (let a of actions) {\n                if (a.fullName === fullName) {\n                    action = a;\n                    break;\n                }\n            }\n        }\n        return action;\n    },\n    async matchActionSimple(query: SearchQuery): Promise<ActionRecord[]> {\n        const request = this.createSearchRequest(query);\n        query = Object.assign(\n            {\n                keywords: \"\",\n                currentFiles: [],\n                currentImage: \"\",\n                currentText: \"\",\n                activeWindow: this.activeWindow,\n            },\n            query,\n        );\n        const actions = await this.listAction(request);\n        const uniqueRemover = new Set<string>();\n        return await this.matchActions(uniqueRemover, actions, query);\n    },\n    async searchActions(\n        uniqueRemover: Set<string>,\n        actions: ActionRecord[],\n        query: SearchQuery,\n    ): Promise<ActionRecord[]> {\n        let results = [];\n        if (!query.keywords) {\n            return results;\n        }\n        for (const a of actions) {\n            if (!a.matches || uniqueRemover.has(a.fullName)) {\n                continue;\n            }\n            let searchScoreMax = 0;\n            let runtimeSearchTitleMatched = \"\";\n            let runtimeMatch = null;\n            for (const m of a.matches) {\n                if (m.type === ActionMatchTypeEnum.TEXT) {\n                    if (\n                        \"minLength\" in m &&\n                        query.keywords.length < m.minLength\n                    ) {\n                        continue;\n                    }\n                    if (\n                        \"maxLength\" in m &&\n                        query.keywords.length > m.maxLength\n                    ) {\n                        continue;\n                    }\n                    const textMatch = PinyinUtil.match(\n                        (m as ActionMatchText).text,\n                        query.keywords,\n                    );\n                    if (\n                        textMatch.matched &&\n                        textMatch.similarity > searchScoreMax\n                    ) {\n                        searchScoreMax = textMatch.similarity;\n                        runtimeSearchTitleMatched = textMatch.inputMark;\n                        runtimeMatch = m;\n                    }\n                } else if (m.type === ActionMatchTypeEnum.KEY) {\n                    if ((m as ActionMatchKey).key === query.keywords) {\n                        searchScoreMax = 1;\n                        runtimeSearchTitleMatched = PinyinUtil.mark(\n                            query.keywords,\n                        );\n                        runtimeMatch = m;\n                    }\n                }\n            }\n            // console.log('searchScoreMax', a.name, searchScoreMax, a.runtime.searchScore > 0)\n            if (searchScoreMax > 0) {\n                a.runtime.searchScore = searchScoreMax;\n                a.runtime.searchTitleMatched = runtimeSearchTitleMatched;\n                a.runtime.match = runtimeMatch;\n                results.push(a);\n                uniqueRemover.add(a.fullName);\n            }\n        }\n        // sort by similarity\n        results = results.sort((a, b) => {\n            return b.runtime.searchScore - a.runtime.searchScore;\n        });\n        return results;\n    },\n    async matchActions(\n        uniqueRemover: Set<string>,\n        actions: ActionRecord[],\n        query: SearchQuery,\n    ): Promise<ActionRecord[]> {\n        let results = [];\n        if (\n            !query.keywords &&\n            !query.currentImage &&\n            !query.currentFiles.length &&\n            !query.currentText &&\n            !query.activeWindow\n        ) {\n            return results;\n        }\n        const keywords = query.currentText || query.keywords;\n        for (const a of actions) {\n            if (!a.matches || uniqueRemover.has(a.fullName)) {\n                continue;\n            }\n            let searchScoreMax = 0;\n            let runtimeSearchTitleMatched = \"\";\n            let runtimeMatch = null;\n            let matchFiles = [];\n            for (const m of a.matches) {\n                if (m.type === ActionMatchTypeEnum.REGEX) {\n                    if (!keywords) {\n                        continue;\n                    }\n                    if (\"minLength\" in m && keywords.length < m.minLength) {\n                        continue;\n                    }\n                    if (\"maxLength\" in m && keywords.length > m.maxLength) {\n                        continue;\n                    }\n                    if (ReUtil.match((m as ActionMatchRegex).regex, keywords)) {\n                        searchScoreMax = 1;\n                        runtimeSearchTitleMatched =\n                            (m as ActionMatchRegex).title || a.title;\n                        runtimeMatch = m;\n                        break;\n                    }\n                } else if (m.type === ActionMatchTypeEnum.FILE) {\n                    let files = query.currentFiles;\n                    if (files.length <= 0) {\n                        continue;\n                    }\n                    // console.log('file', JSON.stringify({m, files}, null, 2))\n                    if (\"filterFileType\" in m) {\n                        if (m.filterFileType === \"file\") {\n                            files = files.filter((f) => f.isFile);\n                        } else if (m.filterFileType === \"directory\") {\n                            files = files.filter((f) => f.isDirectory);\n                        }\n                    }\n                    if (\"filterExtensions\" in m) {\n                        files = files.filter(\n                            (f) =>\n                                f.isFile &&\n                                (\n                                    m as ActionMatchFile\n                                ).filterExtensions.includes(f.fileExt),\n                        );\n                    }\n                    if (\"minCount\" in m && files.length < m.minCount) {\n                        continue;\n                    }\n                    if (\"maxCount\" in m && files.length > m.maxCount) {\n                        continue;\n                    }\n                    if (files.length <= 0) {\n                        continue;\n                    }\n                    searchScoreMax = 1;\n                    runtimeSearchTitleMatched =\n                        (m as ActionMatchFile).title || a.title;\n                    runtimeMatch = m;\n                    matchFiles = files;\n                    break;\n                } else if (m.type === ActionMatchTypeEnum.IMAGE) {\n                    const image = query.currentImage;\n                    if (!image) {\n                        continue;\n                    }\n                    searchScoreMax = 1;\n                    runtimeSearchTitleMatched =\n                        (m as ActionMatchFile).title || a.title;\n                    runtimeMatch = m;\n                } else if (m.type === ActionMatchTypeEnum.WINDOW) {\n                    const activeWindow = query.activeWindow;\n                    if (!activeWindow) {\n                        continue;\n                    }\n                    if (\n                        (m as ActionMatchWindow).nameRegex &&\n                        !ReUtil.match(\n                            (m as ActionMatchWindow).nameRegex,\n                            activeWindow.name,\n                        )\n                    ) {\n                        continue;\n                    }\n                    if (\n                        (m as ActionMatchWindow).titleRegex &&\n                        !ReUtil.match(\n                            (m as ActionMatchWindow).titleRegex,\n                            activeWindow.title,\n                        )\n                    ) {\n                        continue;\n                    }\n                    if ((m as ActionMatchWindow).attrRegex) {\n                        let pass = true;\n                        for (const key in (m as ActionMatchWindow).attrRegex) {\n                            if (\n                                !ReUtil.match(\n                                    (m as ActionMatchWindow).attrRegex[key],\n                                    activeWindow.attr[key],\n                                )\n                            ) {\n                                pass = false;\n                                break;\n                            }\n                        }\n                        if (!pass) {\n                            continue;\n                        }\n                    }\n                    searchScoreMax = 1;\n                    runtimeSearchTitleMatched = a.title;\n                    runtimeMatch = m;\n                    break;\n                } else if (m.type === ActionMatchTypeEnum.EDITOR) {\n                    let files = query.currentFiles;\n                    if (files.length <= 0) {\n                        continue;\n                    }\n                    // console.log('file', JSON.stringify({m, files}, null, 2))\n                    if (\"extensions\" in m) {\n                        files = files.filter(\n                            (f) =>\n                                f.isFile &&\n                                (m as ActionMatchEditor).extensions.includes(\n                                    f.fileExt,\n                                ),\n                        );\n                    }\n                    if (\"fadTypes\" in m) {\n                        files = await ManagerEditor.filterFadType(\n                            files,\n                            (m as ActionMatchEditor).fadTypes,\n                        );\n                    }\n                    if (files.length <= 0) {\n                        continue;\n                    }\n                    searchScoreMax = 1;\n                    runtimeSearchTitleMatched = a.title;\n                    runtimeMatch = m;\n                    matchFiles = files;\n                    break;\n                }\n            }\n            // console.log('searchScoreMax', a.name, searchScoreMax, a.runtime.searchScore > 0)\n            if (searchScoreMax > 0) {\n                a.runtime.searchScore = searchScoreMax;\n                a.runtime.searchTitleMatched = runtimeSearchTitleMatched;\n                a.runtime.match = runtimeMatch;\n                a.runtime.matchFiles = matchFiles;\n                results.push(a);\n                uniqueRemover.add(a.fullName);\n            }\n        }\n        // sort by similarity\n        results = results.sort((a, b) => {\n            return b.runtime.searchScore - a.runtime.searchScore;\n        });\n        return results;\n    },\n    async detachWindowActions(\n        uniqueRemover: Set<string>,\n        actionFullNameMap: Map<string, ActionRecord>,\n    ) {\n        const results = [];\n        const pluginCount = {};\n        for (const win of ManagerWindow.listDetachWindows()) {\n            let actionWeb = null;\n            for (const a of win._plugin.actions) {\n                if (a.type === ActionTypeEnum.WEB) {\n                    actionWeb = a;\n                    break;\n                }\n            }\n            if (win._type && win._type === \"callPage\") {\n                continue;\n            }\n            if (!actionWeb) {\n                continue;\n            }\n            const fullName = actionWeb.pluginName + \"/\" + actionWeb.name;\n            if (actionFullNameMap.has(fullName)) {\n                const action = actionFullNameMap.get(fullName);\n                const actionClone = cloneDeep(action);\n                actionClone.runtime.windowId = win.id;\n                if (pluginCount[actionWeb.pluginName]) {\n                    pluginCount[actionWeb.pluginName]++;\n                } else {\n                    pluginCount[actionWeb.pluginName] = 1;\n                }\n                actionClone.runtime.windowIndex =\n                    pluginCount[actionWeb.pluginName];\n                results.push(actionClone);\n                uniqueRemover.add(fullName);\n            }\n        }\n        for (const r of results) {\n            r.runtime.windowCount = results.filter(\n                (a) => a.pluginName === r.pluginName,\n            ).length;\n        }\n        return results;\n    },\n    async historyActions(\n        uniqueRemover: Set<string>,\n        actionFullNameMap: Map<string, ActionRecord>,\n        query: SearchQuery,\n    ) {\n        const historyActions = await ManagerConfig.getHistoryAction();\n        const results = [];\n        for (const h of historyActions) {\n            const fullName = h.pluginName + \"/\" + h.actionName;\n            if (uniqueRemover.has(fullName)) {\n                continue;\n            }\n            if (actionFullNameMap.has(fullName)) {\n                results.push(actionFullNameMap.get(fullName));\n                uniqueRemover.add(fullName);\n            }\n        }\n        return results;\n    },\n    async pinActions(\n        uniqueRemover: Set<string>,\n        actionFullNameMap: Map<string, ActionRecord>,\n        query: SearchQuery,\n    ) {\n        const pinActions = await ManagerConfig.listPinAction();\n        const results: ActionRecord[] = [];\n        for (const p of pinActions) {\n            const fullName = p.pluginName + \"/\" + p.actionName;\n            if (uniqueRemover.has(fullName)) {\n                continue;\n            }\n            if (actionFullNameMap.has(fullName)) {\n                results.push(actionFullNameMap.get(fullName));\n                uniqueRemover.add(fullName);\n            }\n        }\n        return results;\n    },\n    async sendBroadcast(pluginName: string, type: string, data: any) {\n        for (const view of ManagerWindow.listBrowserViews()) {\n            if (view._plugin && view._plugin.name === pluginName) {\n                Events.sendRaw(view.webContents, \"BROADCAST\", {\n                    type,\n                    data,\n                });\n            }\n        }\n    },\n    async setNotice(\n        notice:\n            | {\n                  text: string;\n                  type?: \"info\" | \"error\" | \"success\";\n                  duration?: number;\n              }\n            | string,\n    ) {\n        if (typeof notice === \"string\") {\n            notice = { text: notice };\n        }\n        notice = Object.assign(\n            {\n                text: \"\",\n                type: \"info\",\n                duration: 0,\n            },\n            notice,\n        );\n        Events.broadcast(\"Notice\", notice, {\n            limit: true,\n            scopes: [\"main\"],\n        });\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/colorPicker.ts",
    "content": "import { FileType, screen as nutScreen, Region } from \"@nut-tree-fork/nut-js\";\nimport { BrowserWindow, ipcMain, nativeImage, screen } from \"electron\";\nimport { uIOhook, UiohookKey } from \"uiohook-napi\";\nimport { t } from \"../../../config/lang\";\nimport { isLinux, isMac, isWin } from \"../../../lib/env\";\nimport { AppsMain } from \"../../app/main\";\nimport { Files } from \"../../file/main\";\n\nlet currentPromise: Promise<void> | null = null;\n\nexport const colorPicker = async (): Promise<void> => {\n    if (currentPromise) {\n        return currentPromise;\n    }\n    currentPromise = new Promise<void>((resolve) => {\n        let magnifierWindow: BrowserWindow | null = null;\n        let isPicking = true;\n        let updating = false;\n        let currentColor: string = \"#FFFFFF\";\n        let bitmaps: {\n            display: any;\n            bitmap: Uint8Array;\n            size: { width: number; height: number };\n            scaleFactor: number;\n            originalPhysicalX: number;\n            originalPhysicalY: number;\n            clampedPhysicalX: number;\n            clampedPhysicalY: number;\n        }[] = [];\n\n        const cleanup = () => {\n            if (magnifierWindow) {\n                magnifierWindow.close();\n                magnifierWindow = null;\n            }\n            ipcMain.off(\"copy-color\", copyHandler);\n        };\n\n        const copyHandler = () => {\n            AppsMain.setClipboardText(currentColor);\n            AppsMain.toast(t(\"plugin.colorCopied\", { color: currentColor }));\n            isPicking = false;\n            resolve();\n            cleanup();\n        };\n\n        ipcMain.on(\"copy-color\", copyHandler);\n\n        // HTML for magnifier\n        const copyShortcut = isMac ? \"Cmd+Shift+C\" : \"Ctrl+Shift+C\";\n        const html = `\n            <!DOCTYPE html>\n            <html>\n            <head>\n                <style>\n                    body {\n                        margin: 0;\n                        overflow: hidden;\n                        background: rgba(255, 255, 255, 0.9);\n                        border-radius: 5px;\n                        display: flex;\n                        gap: 10px;\n                        padding: 10px;\n                        position: relative;\n                    }\n                    #magnifier {\n                        width: 100px;\n                        height: 100px;\n                        border-radius: 5px;\n                    }\n                    .right-panel {\n                        display: flex;\n                        flex-direction: column;\n                        gap: 10px;\n                        flex-grow: 1;\n                        border-radius: 5px;\n                    }\n                    .color-preview {\n                        height: 30px;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        font-family: monospace;\n                        font-size: 14px;\n                        font-weight: bold;\n                        border: 1px solid #000;\n                        border-radius: 5px;\n                    }\n                    .shortcut-text {\n                        font-family: monospace;\n                        font-size: 12px;\n                        color: #666;\n                        display: flex;\n                        align-items: center;\n                    }\n                </style>\n            </head>\n            <body>\n                <canvas id=\"magnifier\" width=\"100\" height=\"100\"></canvas>\n                <div class=\"right-panel\">\n                    <div class=\"color-preview\" id=\"colorPreview\">#FFFFFF</div>\n                    <div class=\"shortcut-text\">${t(\"plugin.colorCopyShortcut\", { shortcut: copyShortcut })}</div>\n                    <div class=\"shortcut-text\">${t(\"plugin.exitEsc\")}</div>\n                </div>\n                <script>\n                    const { ipcRenderer } = require('electron');\n                    const canvas = document.getElementById('magnifier');\n                    const ctx = canvas.getContext('2d');\n                    const dpr = window.devicePixelRatio || 1;\n                    canvas.width = 100 * dpr;\n                    canvas.height = 100 * dpr;\n                    ctx.scale(dpr, dpr);\n                    window.electronAPI = {\n                        updateMagnifier: (imageData) => {\n                            ctx.clearRect(0, 0, 100, 100);\n                            // Draw pixel grid\n                            const pixelSize = 5;\n                            for (let j = 0; j < 20; j++) {\n                                for (let i = 0; i < 20; i++) {\n                                    const index = (j * 20 + i) * 4;\n                                    const r = imageData[index];\n                                    const g = imageData[index + 1];\n                                    const b = imageData[index + 2];\n                                    ctx.fillStyle = \\`rgb(\\${r},\\${g},\\${b})\\`;\n                                    ctx.fillRect(i * pixelSize, j * pixelSize, pixelSize, pixelSize);\n                                }\n                            }\n                            // Draw grid lines\n                            ctx.strokeStyle = 'black';\n                            ctx.lineWidth = 1 / dpr;\n                            for (let x = 0; x <= 100; x += pixelSize) {\n                                ctx.beginPath();\n                                ctx.moveTo(x, 0);\n                                ctx.lineTo(x, 100);\n                                ctx.stroke();\n                            }\n                            for (let y = 0; y <= 100; y += pixelSize) {\n                                ctx.beginPath();\n                                ctx.moveTo(0, y);\n                                ctx.lineTo(100, y);\n                                ctx.stroke();\n                            }\n                            // Draw crosshair at center of middle pixel\n                            ctx.strokeStyle = 'red';\n                            ctx.lineWidth = 2 / dpr;\n                            ctx.beginPath();\n                            ctx.moveTo(50, 0);\n                            ctx.lineTo(50, 100);\n                            ctx.moveTo(0, 50);\n                            ctx.lineTo(100, 50);\n                            ctx.stroke();\n                        },\n                        updateColor: (color) => {\n                            const preview = document.getElementById('colorPreview');\n                            preview.style.backgroundColor = color;\n                            preview.textContent = color;\n                            // 计算亮度\n                            const r = parseInt(color.slice(1,3),16);\n                            const g = parseInt(color.slice(3,5),16);\n                            const b = parseInt(color.slice(5,7),16);\n                            const brightness = (r * 0.299 + g * 0.587 + b * 0.114);\n                            preview.style.color = brightness > 128 ? '#000' : '#fff';\n                        }\n                    };\n                </script>\n            </body>\n            </html>\n        `;\n\n        // Create magnifier window\n        magnifierWindow = new BrowserWindow({\n            width: 350,\n            height: 120,\n            frame: false,\n            transparent: false,\n            alwaysOnTop: true,\n            skipTaskbar: true,\n            resizable: false,\n            show: false,\n            webPreferences: {\n                nodeIntegration: true,\n                contextIsolation: false,\n            },\n        });\n\n        magnifierWindow.loadURL(\n            `data:text/html;charset=utf-8,${encodeURIComponent(html)}`,\n        );\n        magnifierWindow.once(\"ready-to-show\", async () => {\n            magnifierWindow!.on(\"closed\", () => {\n                uIOhook.off(\"mousemove\", mouseMoveCallback);\n                uIOhook.off(\"keydown\", keyDownCallback);\n            });\n            const totalWidth = await nutScreen.width();\n            const totalHeight = await nutScreen.height();\n            const displays = screen.getAllDisplays();\n            const screenshots = await Promise.all(\n                displays.map(async (display) => {\n                    try {\n                        const scaleFactor = display.scaleFactor;\n                        const physicalX = Math.round(\n                            display.bounds.x * scaleFactor,\n                        );\n                        const physicalY = Math.round(\n                            display.bounds.y * scaleFactor,\n                        );\n                        const physicalWidth = Math.round(\n                            display.bounds.width * scaleFactor,\n                        );\n                        const physicalHeight = Math.round(\n                            display.bounds.height * scaleFactor,\n                        );\n                        const originalPhysicalX = physicalX;\n                        const originalPhysicalY = physicalY;\n                        const clampedPhysicalX = Math.max(0, physicalX);\n                        const clampedPhysicalY = Math.max(0, physicalY);\n                        const clampedPhysicalWidth = Math.min(\n                            physicalWidth,\n                            totalWidth - clampedPhysicalX,\n                        );\n                        const clampedPhysicalHeight = Math.min(\n                            physicalHeight,\n                            totalHeight - clampedPhysicalY,\n                        );\n                        const region = new Region(\n                            clampedPhysicalX,\n                            clampedPhysicalY,\n                            clampedPhysicalWidth,\n                            clampedPhysicalHeight,\n                        );\n                        const capturePath = await (\n                            nutScreen.captureRegion as any\n                        )(\n                            await Files.tempName(),\n                            region,\n                            FileType.PNG,\n                            await Files.tempRoot(),\n                        );\n                        // console.log('capturePath', capturePath);\n                        const image = nativeImage.createFromPath(capturePath);\n                        Files.deletes(capturePath).then();\n                        const bitmap = image.getBitmap() as any;\n                        const size = image.getSize();\n                        return {\n                            display,\n                            bitmap,\n                            size,\n                            scaleFactor,\n                            originalPhysicalX,\n                            originalPhysicalY,\n                            clampedPhysicalX,\n                            clampedPhysicalY,\n                        };\n                    } catch (error) {\n                        console.error(\"Error capturing display:\", error);\n                        return null;\n                    }\n                }),\n            );\n            bitmaps = screenshots.filter(\n                (\n                    item,\n                ): item is {\n                    display: any;\n                    bitmap: Uint8Array;\n                    size: { width: number; height: number };\n                    scaleFactor: number;\n                    originalPhysicalX: number;\n                    originalPhysicalY: number;\n                    clampedPhysicalX: number;\n                    clampedPhysicalY: number;\n                } => Boolean(item),\n            );\n            await updateMagnifier();\n            magnifierWindow!.show();\n        });\n\n        const updateMagnifier = async () => {\n            if (!isPicking || updating || bitmaps.length === 0) return;\n            updating = true;\n            try {\n                const mousePos = screen.getCursorScreenPoint();\n                const display = screen.getDisplayNearestPoint(mousePos);\n                const bitmapData = bitmaps.find(\n                    (b) => b.display.id === display.id,\n                );\n                if (!bitmapData) return;\n                const {\n                    bitmap,\n                    size,\n                    scaleFactor,\n                    originalPhysicalX,\n                    originalPhysicalY,\n                    clampedPhysicalX,\n                    clampedPhysicalY,\n                } = bitmapData;\n                const offsetX = Math.round(\n                    (mousePos.x - display.bounds.x) * scaleFactor +\n                        (originalPhysicalX - clampedPhysicalX),\n                );\n                const offsetY = Math.round(\n                    (mousePos.y - display.bounds.y) * scaleFactor +\n                        (originalPhysicalY - clampedPhysicalY),\n                );\n                const colors: number[] = [];\n                for (let dy = -9; dy <= 10; dy++) {\n                    for (let dx = -9; dx <= 10; dx++) {\n                        const px = offsetX + dx;\n                        const py = offsetY + dy;\n                        if (\n                            px < 0 ||\n                            px >= size.width ||\n                            py < 0 ||\n                            py >= size.height\n                        ) {\n                            colors.push(255, 255, 255, 255); // white\n                        } else {\n                            const index = (py * size.width + px) * 4;\n                            const r = bitmap[index + 2];\n                            const g = bitmap[index + 1];\n                            const b = bitmap[index];\n                            const a = bitmap[index + 3];\n                            colors.push(r, g, b, a);\n                        }\n                    }\n                }\n                const imageData = new Uint8ClampedArray(colors);\n                magnifierWindow!.webContents.executeJavaScript(`\n                    window.electronAPI.updateMagnifier(${JSON.stringify(Array.from(imageData))});\n                `);\n                // Get current color at mouse position\n                const centerIndex = (10 * 21 + 10) * 4; // Center pixel\n                const r = colors[centerIndex];\n                const g = colors[centerIndex + 1];\n                const b = colors[centerIndex + 2];\n                const hex =\n                    \"#\" +\n                    r.toString(16).padStart(2, \"0\") +\n                    g.toString(16).padStart(2, \"0\") +\n                    b.toString(16).padStart(2, \"0\");\n                currentColor = hex.toUpperCase();\n                magnifierWindow!.webContents.executeJavaScript(`\n                    window.electronAPI.updateColor('${hex}');\n                `);\n                // Position window in top-right or top-left based on mouse position\n                const windowBounds = magnifierWindow!.getBounds();\n                const isMouseInTopRight =\n                    mousePos.x >\n                        display.bounds.x + display.bounds.width * 0.75 &&\n                    mousePos.y <\n                        display.bounds.y + display.bounds.height * 0.25;\n                let x, y;\n                if (isMouseInTopRight) {\n                    x = display.bounds.x;\n                    y = display.bounds.y;\n                } else {\n                    x =\n                        display.bounds.x +\n                        display.bounds.width -\n                        windowBounds.width;\n                    y = display.bounds.y;\n                }\n                magnifierWindow!.setPosition(x, y);\n            } catch (error) {\n                console.error(\"Error updating magnifier:\", error);\n            } finally {\n                updating = false;\n            }\n        };\n        // Listen to mouse events\n        const mouseMoveCallback = () => {\n            updateMagnifier();\n        };\n        const keyDownCallback = (event: any) => {\n            if (!isPicking) return;\n            if (\n                (isMac &&\n                    event.metaKey &&\n                    event.shiftKey &&\n                    event.keycode === UiohookKey.C) ||\n                ((isWin || isLinux) &&\n                    event.ctrlKey &&\n                    event.shiftKey &&\n                    event.keycode === UiohookKey.C)\n            ) {\n                AppsMain.setClipboardText(currentColor);\n                AppsMain.toast(\n                    t(\"plugin.colorCopied\", { color: currentColor }),\n                );\n                isPicking = false;\n                resolve();\n                cleanup();\n                currentPromise = null;\n            } else if (event.keycode === UiohookKey.Escape) {\n                // ESC to exit\n                isPicking = false;\n                resolve(); // Or currentColor, but exit without copying\n                cleanup();\n                currentPromise = null;\n            }\n        };\n        uIOhook.on(\"mousemove\", mouseMoveCallback);\n        uIOhook.on(\"keydown\", keyDownCallback);\n\n        // Initial update\n        mouseMoveCallback();\n    });\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/event.ts",
    "content": "import {\n    app,\n    BrowserView,\n    BrowserWindow,\n    clipboard,\n    dialog,\n    nativeImage,\n    Notification,\n    screen,\n    shell,\n} from \"electron\";\nimport fs from \"fs\";\nimport { AppConfig } from \"../../../../src/config\";\nimport { CommonConfig } from \"../../../config/common\";\nimport { t } from \"../../../config/lang\";\nimport { WindowConfig } from \"../../../config/window\";\nimport {\n    isLinux,\n    isMac,\n    isWin,\n    platformArch,\n    platformName,\n    platformUUID,\n} from \"../../../lib/env\";\nimport { EncodeUtil } from \"../../../lib/util\";\nimport { Page } from \"../../../page\";\nimport { PagePayment } from \"../../../page/payment\";\nimport { PageUser } from \"../../../page/user\";\nimport { AppPosition } from \"../../app/lib/position\";\nimport { AppsMain } from \"../../app/main\";\nimport { AppRuntime } from \"../../env\";\nimport { Files } from \"../../file/main\";\nimport { KVDBMain } from \"../../kvdb/main\";\nimport { DBError } from \"../../kvdb/types\";\nimport { Log } from \"../../log/main\";\nimport User, { UserApi } from \"../../user/main\";\nimport { ManagerAutomation } from \"../automation\";\nimport { ManagerClipboard } from \"../clipboard\";\nimport {\n    getClipboardFiles,\n    setClipboardFiles,\n} from \"../clipboard/clipboardFiles\";\nimport { ManagerConfig } from \"../config/config\";\nimport { ManagerHotkeySimulate } from \"../hotkey/simulate\";\nimport { executeHooks, executePluginHooks } from \"../lib/hooks\";\nimport { Manager } from \"../manager\";\nimport { PluginContext } from \"../type\";\nimport { ManagerWindow } from \"../window\";\nimport { ManagerPlugin } from \"./index\";\nimport { listModels, modelChat } from \"./llm\";\nimport { PluginLog } from \"./log\";\nimport { ManagerPluginPermission } from \"./permission\";\nimport { screenCapture } from \"./screenCapture\";\n\nconst getHeadHeight = (win: BrowserWindow) => {\n    if (win === AppRuntime.mainWindow) {\n        return WindowConfig.minHeight;\n    } else {\n        return WindowConfig.detachWindowTitleHeight;\n    }\n};\n\nexport const ManagerPluginEvent = {\n    pluginEvents: {} as {\n        [event: string]: PluginContext[];\n    },\n    firePluginEvent: async (event: PluginEvent, data: any) => {\n        if (event in ManagerPluginEvent.pluginEvents) {\n            for (const context of ManagerPluginEvent.pluginEvents[event]) {\n                await executePluginHooks(\n                    context as BrowserView,\n                    \"PluginEvent\",\n                    { event, data },\n                );\n            }\n        }\n    },\n    registerPluginEvent: async (context: PluginContext, data: any) => {\n        // console.log('registerPluginEvent', context._plugin)\n        const { event } = data;\n        if (!(event in ManagerPluginEvent.pluginEvents)) {\n            ManagerPluginEvent.pluginEvents[event] = [];\n        }\n        if (!ManagerPluginPermission.check(context._plugin, \"event\", event)) {\n            return;\n        }\n        ManagerPluginEvent.pluginEvents[event].push(context);\n        for (const e in ManagerPluginEvent.pluginEvents) {\n            ManagerPluginEvent.pluginEvents[e] =\n                ManagerPluginEvent.pluginEvents[e].filter((c) => {\n                    return !!(c as BrowserView).webContents;\n                });\n            if (ManagerPluginEvent.pluginEvents[e].length === 0) {\n                delete ManagerPluginEvent.pluginEvents[e];\n            }\n        }\n    },\n    unregisterPluginEvent: async (context: PluginContext, data: any) => {\n        const { event } = data;\n        if (!(event in ManagerPluginEvent.pluginEvents)) {\n            return;\n        }\n        ManagerPluginEvent.pluginEvents[event] =\n            ManagerPluginEvent.pluginEvents[event].filter((c) => c !== context);\n    },\n    registerHotkey: async (context: PluginContext, data: any) => {\n        if (!context._event) {\n            context._event = {};\n        }\n        if (!context._event[\"Hotkey\"]) {\n            context._event[\"Hotkey\"] = [];\n        }\n        const { id, hotkeys } = data;\n        context._event[\"Hotkey\"].push({\n            id,\n            hotkeys,\n        });\n    },\n    unregisterHotkeyAll: async (context: PluginContext, data: any) => {\n        if (!context._event || !context._event[\"HotKey\"]) {\n            return;\n        }\n        context._event[\"Hotkey\"] = [];\n    },\n    isMacOs: async (context: PluginContext, data: any) => {\n        return isMac;\n    },\n    isWindows: async (context: PluginContext, data: any) => {\n        return isWin;\n    },\n    isLinux: async (context: PluginContext, data: any) => {\n        return isLinux;\n    },\n    getPlatformArch: async (context: PluginContext, data: any) => {\n        return platformArch();\n    },\n    isMainWindowShown: async (context: PluginContext, data: any) => {\n        const win = AppRuntime.mainWindow;\n        return win.isVisible();\n    },\n    isMainWindowFocused: async (context: PluginContext, data: any) => {\n        const win = AppRuntime.mainWindow;\n        return win.isFocused();\n    },\n    hideMainWindow: async (context: PluginContext, data: any) => {\n        AppRuntime.mainWindow.hide();\n    },\n    showMainWindow: async (context: PluginContext, data: any) => {\n        Manager.selectedContent = null;\n        // Manager.selectedContent = await ManagerClipboard.getSelectedContent()\n        Manager.activeWindow = await ManagerAutomation.getActiveWindow();\n        const { x: wx, y: wy } = AppPosition.get(\n            \"main\",\n            (screenX, screenY, screenWidth, screenHeight) => {\n                // console.log('calculator', {screenX, screenY, screenWidth, screenHeight});\n                return {\n                    x: screenX + screenWidth / 2 - WindowConfig.mainWidth / 2,\n                    y: screenY + screenHeight / 8,\n                };\n            },\n        );\n        const win = AppRuntime.mainWindow;\n        win.setAlwaysOnTop(false);\n        win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });\n        win.focus();\n        win.setVisibleOnAllWorkspaces(false, {\n            visibleOnFullScreen: true,\n        });\n        win.setPosition(wx, wy);\n        win.show();\n    },\n    isFastPanelWindowShown: async (context: PluginContext, data: any) => {\n        const win = AppRuntime.fastPanelWindow;\n        return win.isVisible() && win.isFocused();\n    },\n    showFastPanelWindow: async (context: PluginContext, data: any) => {\n        Manager.selectedContent = await ManagerClipboard.getSelectedContent();\n        Manager.activeWindow = await ManagerAutomation.getActiveWindow();\n        const win = AppRuntime.fastPanelWindow;\n        const { x, y } = AppPosition.getContextMenuPosition(\n            WindowConfig.fastPanelWidth,\n            WindowConfig.fastPanelHeight,\n        );\n        win.setAlwaysOnTop(false);\n        win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });\n        win.focus();\n        win.setVisibleOnAllWorkspaces(false, {\n            visibleOnFullScreen: true,\n        });\n        win.setPosition(x, y);\n        win.show();\n    },\n    hideFastPanelWindow: async (context: PluginContext, data: any) => {\n        const win = AppRuntime.fastPanelWindow;\n        win.hide();\n    },\n    showOpenDialog: async (context: PluginContext, data: any) => {\n        return dialog.showOpenDialogSync(context._window, data);\n    },\n    showSaveDialog: async (context: PluginContext, data: any) => {\n        return dialog.showSaveDialogSync(context._window, data);\n    },\n    setExpendHeight: async (context: PluginContext, data: any) => {\n        const targetHeight = data as number;\n        const win = context._window;\n        win.setSize(win.getSize()[0], targetHeight);\n        const screenPoint = screen.getCursorScreenPoint();\n        const display = screen.getDisplayNearestPoint(screenPoint);\n        const position =\n            win.getPosition()[1] + targetHeight > display.bounds.height\n                ? targetHeight - getHeadHeight(win)\n                : 0;\n        // originWindow.webContents.executeJavaScript(\n        //     `window.setPosition && typeof window.setPosition === \"function\" && window.setPosition(${position})`\n        // );\n    },\n    setSubInput: async (context: PluginContext, data: any) => {\n        const win = context._window;\n        const payload = {\n            placeholder: data.placeholder,\n            isFocus: data.isFocus,\n            isVisible: data.isVisible,\n        };\n        if (data.isFocus) {\n            win.webContents.focus();\n        }\n        await executeHooks(win, \"SetSubInput\", payload);\n    },\n    removeSubInput: async (context: PluginContext, data: any) => {\n        await executeHooks(context._window, \"RemoveSubInput\");\n    },\n    setSubInputValue: async (context: PluginContext, data: any) => {\n        const { text } = data;\n        await executeHooks(context._window, \"SetSubInputValue\", text);\n        // this.sendSubInputChangeEvent({ data });\n    },\n    subInputBlur: async (context: PluginContext, data: any) => {\n        (context as BrowserView).webContents.focus();\n    },\n    getPluginRoot: async (context: PluginContext, data: any) => {\n        if (context._plugin.runtime && context._plugin.runtime.root) {\n            return context._plugin.runtime.root;\n        }\n        return null;\n    },\n    getPluginConfig: async (context: PluginContext, data: any) => {\n        if (context._plugin) {\n            return context._plugin;\n        }\n        return null;\n    },\n    getPluginInfo: async (context: PluginContext, data: any) => {\n        if (context._plugin) {\n            return ManagerPlugin.getInfo(context._plugin);\n        }\n        return null;\n    },\n    getPluginEnv: async (context: PluginContext, data: any) => {\n        if (context._plugin) {\n            if (context._plugin.env && context._plugin.env.env) {\n                return context._plugin.env.env;\n            }\n        }\n        return \"prod\";\n    },\n    getQuery: async (\n        context: PluginContext,\n        data: any,\n    ): Promise<SearchQuery> => {\n        const { requestId } = data;\n        return (\n            Manager.getSearchRequestQuery(requestId) || {\n                keywords: \"\",\n                currentFiles: [],\n                currentImage: \"\",\n                currentText: \"\",\n                selectedContent: null,\n            }\n        );\n    },\n    getPath: async (context: PluginContext, data: any) => {\n        return app.getPath(data.name);\n    },\n    showToast: async (context: PluginContext, data: any) => {\n        let { body, options } = data;\n        options = Object.assign(\n            {\n                duration: 0,\n                status: \"success\",\n            },\n            options,\n        );\n        AppsMain.toast(body, options);\n    },\n    showNotification: async (context: PluginContext, data: any) => {\n        let { body, clickActionName } = data;\n        if (!Notification.isSupported()) {\n            Log.error(\n                \"ManagerEvent.showNotification.Notification is not supported\",\n            );\n            return;\n        }\n        if (\"string\" != typeof body) {\n            body = String(body);\n        }\n        const plugin = context._plugin;\n        let icon = plugin.logo;\n        if (icon && icon.startsWith(\"file://\")) {\n            icon = icon.substring(7);\n        }\n        const notify = new Notification({\n            title: plugin ? plugin.title : null,\n            body,\n            icon,\n        });\n        notify.show();\n    },\n    showMessageBox: async (context: PluginContext, data: any) => {\n        const { title, message, yes, no } = data;\n        const buttons = [];\n        if (yes) {\n            buttons.push(yes);\n        }\n        if (no) {\n            buttons.push(no);\n        }\n        const result = await dialog.showMessageBox({\n            type: \"info\",\n            title: title || t(\"common.tip\"),\n            message: message,\n            buttons: buttons,\n            defaultId: 0,\n            cancelId: 1,\n        });\n        if (result.response === 0) {\n            return true;\n        }\n        return false;\n    },\n    copyImage: async (context: PluginContext, data: any) => {\n        const { image } = data;\n        let imageData;\n        if (image.startsWith(\"data:image/\")) {\n            imageData = nativeImage.createFromDataURL(image);\n        } else {\n            imageData = nativeImage.createFromPath(image);\n        }\n        clipboard.writeImage(imageData);\n    },\n    copyText: async (context: PluginContext, data: any) => {\n        clipboard.writeText(String(data.text));\n    },\n    copyFile: async (context: PluginContext, data: any) => {\n        let { file } = data;\n        if (file) {\n            if (!Array.isArray(file)) {\n                file = [file];\n            }\n            setClipboardFiles(file);\n            return true;\n        }\n        return false;\n    },\n    getClipboardText: async (context: PluginContext, data: any) => {\n        return AppsMain.getClipboardText();\n    },\n    getClipboardImage: async (context: PluginContext, data: any) => {\n        return AppsMain.getClipboardImage();\n    },\n    getClipboardFiles: async (context: PluginContext, data: any) => {\n        return getClipboardFiles();\n    },\n    listClipboardItems: async (context: PluginContext, data: any) => {\n        if (\n            !ManagerPluginPermission.checkPermit(\n                context._plugin,\n                \"ClipboardManage\",\n            )\n        ) {\n            throw new Error(\"Missing permission: ClipboardManage\");\n        }\n        let { option } = data;\n        option = Object.assign(\n            {\n                limit: -1,\n            },\n            option,\n        );\n        return await ManagerClipboard.list(option.limit);\n    },\n    deleteClipboardItem: async (context: PluginContext, data: any) => {\n        if (\n            !ManagerPluginPermission.checkPermit(\n                context._plugin,\n                \"ClipboardManage\",\n            )\n        ) {\n            throw new Error(\"Missing permission: ClipboardManage\");\n        }\n        const { timestamp } = data;\n        if (!timestamp) {\n            throw new Error(\"Timestamp is required to delete clipboard item.\");\n        }\n        return await ManagerClipboard.delete(timestamp);\n    },\n    clearClipboardItems: async (context: PluginContext, data: any) => {\n        if (\n            !ManagerPluginPermission.checkPermit(\n                context._plugin,\n                \"ClipboardManage\",\n            )\n        ) {\n            throw new Error(\"Missing permission: ClipboardManage\");\n        }\n        return await ManagerClipboard.clear();\n    },\n    shellBeep: async (context: PluginContext, data: any) => {\n        shell.beep();\n    },\n    getFileIcon: async (context: PluginContext, data: any) => {\n        const nativeImage = await app.getFileIcon(data.path, {\n            size: \"normal\",\n        });\n        return nativeImage.toDataURL();\n    },\n    shellShowItemInFolder: async (context: PluginContext, data: any) => {\n        shell.showItemInFolder(data.path);\n    },\n    simulateKeyboardTap: async (context: PluginContext, data: any) => {\n        const { key, modifiers } = data;\n        // 'ctrl' | 'shift' | 'command' | 'option' | 'alt'\n        const modifiersNumber = modifiers.map((m) => {\n            switch (m) {\n                case \"ctrl\":\n                    return ManagerHotkeySimulate.toCode(\"Ctrl\");\n                case \"shift\":\n                    return ManagerHotkeySimulate.toCode(\"Shift\");\n                case \"command\":\n                    return ManagerHotkeySimulate.toCode(\"Meta\");\n                case \"option\":\n                case \"alt\":\n                    return ManagerHotkeySimulate.toCode(\"Alt\");\n            }\n        });\n        ManagerHotkeySimulate.keyTap(\n            ManagerHotkeySimulate.toCode(key),\n            modifiersNumber,\n        );\n    },\n    simulateTypeString: async (context: PluginContext, data: any) => {\n        const { text } = data;\n        await ManagerAutomation.typeString(text);\n    },\n    simulateMouseToggle: async (context: PluginContext, data: any) => {\n        const { type, button } = data;\n        await ManagerAutomation.mouseToggle(type, button);\n    },\n    simulateMouseMove: async (context: PluginContext, data: any) => {\n        const { x, y } = data;\n        await ManagerAutomation.moveMouse(x, y);\n    },\n    simulateMouseClick: async (context: PluginContext, data: any) => {\n        const { button, double } = data;\n        await ManagerAutomation.mouseClick(button, double);\n    },\n    screenCapture: async (context: PluginContext, data: any) => {\n        screenCapture((image: string) => {\n            if (context[\"_screenCaptureCallback\"]) {\n                context[\"_screenCaptureCallback\"]({ image });\n            } else {\n                executePluginHooks(context as BrowserView, \"ScreenCapture\", {\n                    image: image,\n                });\n            }\n        });\n    },\n    getNativeId: async (context: PluginContext, data: any) => {\n        return [platformName(), EncodeUtil.md5(platformUUID())].join(\"_\");\n    },\n    getAppVersion: async (context: PluginContext, data: any) => {\n        return AppConfig.version;\n    },\n    outPlugin: async (context: PluginContext, data: any) => {\n        const option: any = {};\n        if (context && context._window) {\n            option.window = context._window;\n        }\n        await ManagerWindow.close(context._plugin, option);\n    },\n    isDarkColors: async (context: PluginContext, data: any) => {\n        return await AppsMain.shouldDarkMode();\n    },\n    showUserLogin: async (context: PluginContext, data: any) => {\n        await PageUser.open({\n            parent: context._window,\n        });\n    },\n    getUser: async (\n        context: PluginContext,\n        data: any,\n    ): Promise<{\n        isLogin: boolean;\n        avatar: string;\n        nickname: string;\n        vipFlag: string;\n        deviceCode: string;\n        openId: string;\n    } | null> => {\n        const info = await User.get();\n        const user = info.user;\n        const result = {\n            isLogin: !!(user && user.id),\n            avatar: user.avatar || \"\",\n            nickname: user.name || \"\",\n            vipFlag: info.data?.vip?.flag || \"\",\n            deviceCode: user.deviceCode,\n            openId: \"\",\n        };\n        if (result.isLogin) {\n            const res = await UserApi.post<{\n                openId: string;\n            }>(\n                \"client/getUser\",\n                {\n                    pluginName: context._plugin.name,\n                },\n                {\n                    throwException: false,\n                },\n            );\n            if (res.code === 0) {\n                if (res.data) {\n                    result.openId = res.data.openId;\n                }\n            }\n        }\n        return result;\n    },\n    redirect: async (context: PluginContext, data: any) => {\n        let { keywordsOrAction, query } = data;\n        query = Object.assign(\n            {\n                keywords: \"\",\n                currentFiles: [],\n                currentImage: \"\",\n                currentText: \"\",\n            },\n            query,\n        );\n        const action = await Manager.searchOneAction(keywordsOrAction, query);\n        // console.log(\"redirect\", {keywordsOrAction, query, action});\n        if (!action) {\n            ManagerPluginEvent.showToast(context, {\n                body: t(\"plugin.actionNotFound\"),\n            });\n            return;\n        }\n        await Manager.openAction(action);\n    },\n    getActions: async (context: PluginContext, data: any) => {\n        let { names } = data;\n        names = names || [];\n        const customActions = await ManagerConfig.getCustomAction();\n        const plugin = context._plugin;\n        if (!(plugin.name in customActions)) {\n            return [];\n        }\n        return customActions[plugin.name]\n            .filter((m) => {\n                if (names.length > 0) {\n                    return names.includes(m.name);\n                }\n                return true;\n            })\n            .map((m) => {\n                return m;\n            });\n    },\n    setAction: async (context: PluginContext, data: any) => {\n        const { action } = data;\n        const plugin = context._plugin;\n        await ManagerConfig.addCustomAction(plugin, action);\n    },\n    removeAction: async (context: PluginContext, data: any) => {\n        const { name } = data;\n        const plugin = context._plugin;\n        await ManagerConfig.removeCustomAction(plugin, name);\n    },\n\n    callPage: async (context: PluginContext, data_: any) => {\n        let { type, data, option } = data_;\n        option = Object.assign(\n            {\n                waitReadyTimeout: 10 * 1000,\n                timeout: 60 * 1000,\n                showWindow: true,\n                autoClose: true,\n            },\n            option,\n        ) as CallPageOption;\n        const plugin = context._plugin;\n        return new Promise<any>((resolve, reject) => {\n            ManagerWindow.open(plugin, null, {\n                type: \"callPage\",\n                callPage: {\n                    type,\n                    data,\n                    option,\n                    onResult(result) {\n                        if (result.code === 0) {\n                            resolve(result.data);\n                        } else {\n                            reject(new Error(result.msg || \"Error\"));\n                        }\n                    },\n                },\n            });\n        });\n    },\n\n    setRemoteWebRuntime: async (context: PluginContext, data: any) => {\n        const { info } = data;\n        const plugin = context._plugin;\n        plugin.runtime.remoteWeb = {\n            userAgent: info.userAgent || \"\",\n            urlMap: info.urlMap || {},\n            types: info.types || [],\n            blocks: info.blocks || [],\n            domains: info.domains || [],\n        };\n    },\n\n    llmListModels: async (context: PluginContext, data: any) => {\n        return listModels();\n    },\n\n    llmChat: async (context: PluginContext, data: any) => {\n        const { callInfo } = data;\n        try {\n            return modelChat(\n                callInfo.providerId,\n                callInfo.modelId,\n                callInfo.message,\n            );\n        } catch (e) {\n            return {\n                code: -1,\n                msg: `Request failed: ${e instanceof Error ? e.message : String(e)}`,\n            };\n        }\n    },\n\n    logInfo: async (context: PluginContext, data: any) => {\n        const { label, logData } = data;\n        PluginLog.info(context._plugin.name, label, logData);\n    },\n    logError: async (context: PluginContext, data: any) => {\n        const { label, logData } = data;\n        PluginLog.error(context._plugin.name, label, logData);\n    },\n    logPath: async (context: PluginContext, data: any) => {\n        return Log.appPath(PluginLog.name(context._plugin.name));\n    },\n    logShow: async (context: PluginContext, data: any) => {\n        const p = Log.appPath(PluginLog.name(context._plugin.name));\n        Page.open(\"log\", {\n            log: p,\n        });\n    },\n\n    addLaunch: async (context: PluginContext, data: any) => {\n        const { keyword, name, hotkey } = data;\n        if (!keyword || !name || !hotkey) {\n            throw new Error(\"Keyword, name and hotkey are required.\");\n        }\n        const hotkeyConvert = {\n            key: hotkey.key,\n            // Alt Option\n            altKey: false,\n            // Ctrl Control\n            ctrlKey: false,\n            // Command Win\n            metaKey: false,\n            // Shift\n            shiftKey: false,\n            times: 1,\n        };\n        const modifiers = hotkey.modifiers || [];\n        // \"Control\" | \"Option\" | \"Command\" | \"Ctrl\" | \"Alt\" | \"Win\" | \"Meta\" | \"Shift\"\n        if (modifiers.includes(\"Control\") || modifiers.includes(\"Ctrl\")) {\n            hotkeyConvert.ctrlKey = true;\n        }\n        if (modifiers.includes(\"Option\") || modifiers.includes(\"Alt\")) {\n            hotkeyConvert.altKey = true;\n        }\n        if (\n            modifiers.includes(\"Command\") ||\n            modifiers.includes(\"Win\") ||\n            modifiers.includes(\"Meta\")\n        ) {\n            hotkeyConvert.metaKey = true;\n        }\n        if (modifiers.includes(\"Shift\")) {\n            hotkeyConvert.shiftKey = true;\n        }\n        const records = await ManagerConfig.listLaunch();\n        const exists = records.find((m) => {\n            return (\n                m.type === \"plugin\" &&\n                m.pluginName === context._plugin.name &&\n                m.keyword === keyword\n            );\n        });\n        if (exists) {\n            throw new Error(`Launch with keyword \"${keyword}\" already exists.`);\n        } else {\n            records.push({\n                type: \"plugin\",\n                pluginName: context._plugin.name,\n                keyword,\n                name,\n                hotkey: hotkeyConvert,\n            });\n        }\n        await ManagerConfig.updateLaunch(records);\n    },\n    removeLaunch: async (context: PluginContext, data: any) => {\n        const { keyword } = data;\n        const records = await ManagerConfig.listLaunch();\n        const index = records.findIndex((m) => {\n            return (\n                m.type === \"plugin\" &&\n                m.pluginName === context._plugin.name &&\n                m.keyword === keyword\n            );\n        });\n        if (index >= 0) {\n            records.splice(index, 1);\n            await ManagerConfig.updateLaunch(records);\n        } else {\n            throw new Error(`Launch with keyword \"${keyword}\" not found.`);\n        }\n    },\n\n    activateLatestWindow: async (context: PluginContext, data: any) => {\n        await ManagerAutomation.activateLatestWindow();\n    },\n\n    getUserAccessToken: async (context: PluginContext, data: any) => {\n        const res = await UserApi.post<{\n            token: string;\n            expireAt: number;\n        }>(\"client/getUserAccessToken\", {\n            pluginName: context._plugin.name,\n        });\n        return {\n            token: res.data.token,\n            expireAt: res.data.expireAt,\n        };\n    },\n\n    listGoods: async (context: PluginContext, data: any) => {\n        const { query } = data;\n        const res = await UserApi.post<{\n            total: number;\n            records: {\n                id: string;\n                title: string;\n                cover: string;\n                priceType: \"fixed\" | \"dynamic\";\n                fixedPrice: string;\n                description: string;\n            }[];\n        }>(\"client/listGoods\", {\n            pluginName: context._plugin.name,\n            query,\n        });\n        return res.data.records;\n    },\n\n    openGoodsPayment: async (context: PluginContext, data: any) => {\n        const { options } = data as {\n            options: {\n                goodsId: string;\n                price?: string;\n                outOrderId?: string;\n                outParam?: string;\n            };\n        };\n        const payResult = {\n            paySuccess: false,\n        };\n        return new Promise((resolve, reject) => {\n            let watchUrl = null as any;\n            let controller = null as any;\n            PagePayment.open({\n                parent: context._window,\n                onRefresh: async () => {\n                    // console.log('onRefresh')\n                    const res = await UserApi.post<{\n                        payUrl: string;\n                        watchUrl: string;\n                        payExpireSeconds: number;\n                        body: string;\n                    }>(\"client/createGoodsOrder\", {\n                        pluginName: context._plugin.name,\n                        ...options,\n                    });\n                    watchUrl = res.data.watchUrl;\n                    return {\n                        payUrl: res.data.payUrl,\n                        watchUrl: res.data.watchUrl,\n                        payExpireSeconds: res.data.payExpireSeconds,\n                        body: res.data.body,\n                    };\n                },\n                onWatch: async () => {\n                    // console.log('onWatch')\n                    let status = \"Error\" as\n                        | \"WaitPay\"\n                        | \"Scanned\"\n                        | \"Payed\"\n                        | \"Expired\"\n                        | \"Error\";\n                    const res = await UserApi.post<{\n                        status: \"unknown\" | \"WaitPay\" | \"Payed\";\n                        scanStatus: null | \"Scanned\";\n                    }>(watchUrl, {});\n                    if (res.data.status === \"WaitPay\") {\n                        if (res.data.scanStatus === \"Scanned\") {\n                            status = \"Scanned\";\n                        } else {\n                            status = \"WaitPay\";\n                        }\n                    } else if (res.data.status === \"Payed\") {\n                        status = \"Payed\";\n                    } else if (res.data.status === \"unknown\") {\n                        status = \"Error\";\n                    }\n                    if (\"Payed\" === status) {\n                        payResult.paySuccess = true;\n                        setTimeout(() => {\n                            controller.close();\n                        }, 3000);\n                    }\n                    // console.log('watch', status, res)\n                    return {\n                        status,\n                    };\n                },\n                onClose: async () => {\n                    resolve(payResult);\n                },\n            }).then((c) => {\n                controller = c;\n            });\n        });\n    },\n\n    queryGoodsOrders: async (context: PluginContext, data: any) => {\n        const { options } = data as {\n            options: {\n                goodsId?: string;\n                page?: number;\n                pageSize?: number;\n            };\n        };\n        const res = await UserApi.post<{\n            page: number;\n            total: number;\n            records: {\n                id: string;\n                goodsId: string;\n                status: \"Paid\" | \"Unpaid\";\n            }[];\n        }>(\"client/queryGoodsOrders\", {\n            pluginName: context._plugin.name,\n            ...options,\n        });\n        return {\n            page: res.data.page,\n            total: res.data.total,\n            records: res.data.records,\n        };\n    },\n\n    apiPost: async (context: PluginContext, data: any) => {\n        if (!ManagerPluginPermission.check(context._plugin, \"basic\", \"Api\")) {\n            return;\n        }\n        const { url, body, option } = data;\n        const res = await UserApi.post(url, body || {}, {\n            throwException: false,\n        });\n        return res;\n    },\n\n    // file\n    fileExists: async (context: PluginContext, data: any): Promise<boolean> => {\n        if (!ManagerPluginPermission.check(context._plugin, \"basic\", \"File\")) {\n            return false;\n        }\n        const { path } = data;\n        return await Files.exists(path, {\n            isDataPath: false,\n        });\n    },\n    fileRead: async (context: PluginContext, data: any) => {\n        if (!ManagerPluginPermission.check(context._plugin, \"basic\", \"File\")) {\n            return;\n        }\n        const { path, format } = data;\n        if (\"buffer\" === format) {\n            return await Files.readBuffer(path);\n        }\n        if (\"base64\" === format) {\n            const content = await Files.readBuffer(path);\n            return content.toString(\"base64\");\n        }\n        return await Files.read(path, {\n            isDataPath: false,\n            encoding: \"utf-8\",\n        });\n    },\n    fileWrite: async (context: PluginContext, data: any) => {\n        if (!ManagerPluginPermission.check(context._plugin, \"basic\", \"File\")) {\n            return;\n        }\n        let { path, data: content, option } = data;\n        option = Object.assign(\n            {\n                isBase64: false,\n            },\n            option,\n        );\n        if (typeof content === \"string\") {\n            if (option.isBase64) {\n                if (content.startsWith(\"data:\")) {\n                    content = content.split(\",\")[1];\n                }\n                content = Buffer.from(content, \"base64\");\n                return await Files.writeBuffer(path, content);\n            }\n            return await Files.write(path, content);\n        }\n        return await Files.writeBuffer(path, content);\n    },\n    fileRemove: async (context: PluginContext, data: any): Promise<void> => {\n        if (!ManagerPluginPermission.check(context._plugin, \"basic\", \"File\")) {\n            return;\n        }\n        const { path } = data;\n        return await Files.deletes(path, {\n            isDataPath: false,\n        });\n    },\n    fileExt: async (context: PluginContext, data: any) => {\n        const { path } = data;\n        const ext = Files.ext(path);\n        return ext ? ext : \"\";\n    },\n    fileWriteTemp: async (context: PluginContext, data_: any) => {\n        if (!ManagerPluginPermission.check(context._plugin, \"basic\", \"File\")) {\n            return;\n        }\n        let { ext, data, option } = data_;\n        option = Object.assign(\n            {\n                isBase64: false,\n            },\n            option,\n        );\n        const tempPath = await Files.temp(ext);\n        if (option?.isBase64) {\n            // remove prefix data:image/svg+xml;base64,\n            if ((data as string).startsWith(\"data:\")) {\n                data = (data as string).split(\",\")[1];\n            }\n            data = Buffer.from(data as string, \"base64\");\n        }\n        fs.writeFileSync(tempPath, data as Uint8Array);\n        return tempPath;\n    },\n\n    // db\n    dbPut: async (context: PluginContext, data: any) => {\n        return await KVDBMain.put(context._plugin.name, data.doc);\n    },\n    dbGet: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.get(context._plugin.name, data.id);\n    },\n    dbRemove: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.remove(context._plugin.name, data.doc);\n    },\n    dbBulkDocs: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.bulkDocs(context._plugin.name, data.docs);\n    },\n    dbAllDocs: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.allDocs(context._plugin.name, data.key);\n    },\n    dbPostAttachment: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.postAttachment(\n            context._plugin.name,\n            data.docId,\n            data.attachment,\n            data.type,\n        );\n    },\n    dbGetAttachment: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.getAttachment(context._plugin.name, data.docId);\n    },\n    dbGetAttachmentType: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        return await KVDBMain.getAttachmentType(\n            context._plugin.name,\n            data.docId,\n        );\n    },\n\n    // dbStorage\n    dbStorageSetItem: async (context: PluginContext, data: any) => {\n        // const plugin = ManagerWindow.getPluginByWindow(win);\n        const plugin = context._plugin;\n        const { key, value } = data;\n        const id = `${CommonConfig.dbPluginStorageIdPrefix}/${key}`;\n        const doc = { _id: id, data: value, _rev: undefined };\n        const result = await KVDBMain.get(plugin.name, id);\n        if (result) {\n            doc._rev = result._rev;\n        }\n        const res = await KVDBMain.put(plugin.name, doc);\n        if ((res as DBError).error) throw new Error((res as DBError).message);\n    },\n    dbStorageGetItem: async (context: PluginContext, data: any) => {\n        const plugin = context._plugin;\n        const { key } = data;\n        const id = `${CommonConfig.dbPluginStorageIdPrefix}/${key}`;\n        const result = await KVDBMain.get(plugin.name, id);\n        return result ? result.data : null;\n    },\n    dbStorageRemoveItem: async (context: PluginContext, data: any) => {\n        const plugin = context._plugin;\n        const { key } = data;\n        const id = `${CommonConfig.dbPluginStorageIdPrefix}/${key}`;\n        const result = await KVDBMain.get(plugin.name, id);\n        if (!result) return;\n        await KVDBMain.remove(plugin.name, result);\n    },\n    detachSetTitle: async (context: PluginContext, data: any) => {\n        const { title } = data;\n        await executeHooks(context._window, \"DetachSet\", {\n            title,\n        });\n    },\n    detachSetOperates: async (context: PluginContext, data: any) => {\n        const { operates } = data;\n        await executeHooks(context._window, \"DetachSet\", {\n            operates,\n        });\n    },\n    detachSetPosition: async (context: PluginContext, data: any) => {\n        const { position } = data;\n        const win = context._window;\n        const winSize = win.getSize();\n        const { x, y } = AppsMain.calcPositionInCurrentDisplay(\n            position,\n            winSize[0],\n            winSize[1],\n        );\n        win.setPosition(x, y);\n    },\n    detachSetAlwaysOnTop: async (context: PluginContext, data: any) => {\n        const { alwaysOnTop } = data;\n        const win = context._window;\n        win.setAlwaysOnTop(alwaysOnTop);\n        await executeHooks(context._window, \"DetachSet\", {\n            alwaysOnTop,\n        });\n    },\n    detachSetSize: async (context: PluginContext, data: any) => {\n        const { width, height } = data;\n        const win = context._window;\n        win.setSize(width, height);\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/http.ts",
    "content": "import express from \"express\";\nimport Apps from \"../../app\";\nimport { Log } from \"../../log/main\";\nimport { ManagerPlugin } from \"./index\";\nimport { PluginRecord } from \"../../../../src/types/Manager\";\nimport { serveMcpRPC, serveMcpSSE } from \"./httpMCP\";\n\nasync function servePluginStatic(req, res) {\n    const paths: string[] = req.params.path;\n    if (!paths || !paths.length) {\n        res.status(404).send(\"Plugin Static Server : Not Found\");\n        return;\n    }\n    // console.log('servePluginStatic', paths);\n    const pluginName = paths.shift();\n    const pluginFile = paths.join(\"/\");\n    const plugin: PluginRecord = await ManagerPlugin.get(pluginName);\n    if (!plugin) {\n        res.status(404).send(\"Plugin Static Server : Not Found\");\n        return;\n    }\n    if (!plugin.setting?.httpEntry) {\n        res.status(404).send(\n            \"Plugin Static Server : Plugin HTTP Entry Not Enabled\",\n        );\n        return;\n    }\n    express.static(plugin.runtime.root)(\n        Object.assign(req, { url: `/${pluginFile}` }),\n        res,\n        (err) => {\n            if (err) {\n                res.status(500).send(\"Plugin Static Server : \" + err.message);\n            } else {\n                res.status(404).send(\"Plugin Static Server : Not Found\");\n            }\n        },\n    );\n}\n\nexport const PluginHttp = {\n    app: null,\n    port: 61000,\n    ip: \"127.0.0.1\",\n    async init() {\n        PluginHttp.app = express();\n        PluginHttp.app.use(express.json());\n        PluginHttp.app.all(\"/plugin/*path\", servePluginStatic);\n        PluginHttp.app.post(\"/mcp\", serveMcpRPC);\n        PluginHttp.app.get(\"/mcp\", serveMcpSSE);\n        PluginHttp.port = await Apps.availablePort(PluginHttp.port);\n        return new Promise((resolve, reject) => {\n            PluginHttp.app.listen(PluginHttp.port, PluginHttp.ip, () => {\n                Log.info(\"PluginHttp.Listen\", { port: PluginHttp.port });\n            });\n        });\n    },\n    async getMcpServer() {\n        if (!PluginHttp.app) {\n            await PluginHttp.init();\n        }\n        return `http://${PluginHttp.ip}:${PluginHttp.port}/mcp`;\n    },\n    async url(pluginName: string, filePath: string): Promise<string> {\n        if (!PluginHttp.app) {\n            return new Promise((resolve) => {\n                setTimeout(() => {\n                    resolve(PluginHttp.url(pluginName, filePath));\n                }, 100);\n            });\n        }\n        if (!pluginName || !filePath) {\n            throw new Error(\"Plugin name and file path are required\");\n        }\n        return `http://${PluginHttp.ip}:${PluginHttp.port}/plugin/${pluginName}/${filePath}`;\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/httpMCP.ts",
    "content": "import { ManagerPlugin } from \"./index\";\nimport { Log } from \"../../log/main\";\nimport { MCPToolsRecord } from \"../../../../src/types/Manager\";\nimport { ManagerBackend } from \"../backend\";\nimport { PluginLog } from \"./log\";\n\nconst clients = new Map<any, any>();\n\nexport async function serveMcpSSE(req, res) {\n    res.setHeader(\"Content-Type\", \"text/event-stream\");\n    res.setHeader(\"Cache-Control\", \"no-cache\");\n    res.setHeader(\"Connection\", \"keep-alive\");\n    res.write(`event: ready\\n`);\n    res.write(`data: {}\\n\\n`);\n    clients.set(res, true);\n    Log.info(\"MCPServer.SSE.Connected\", { total: clients.size });\n    req.on(\"close\", () => {\n        clients.delete(res);\n        Log.info(\"MCPServer.SSE.Disconnected\", { total: clients.size });\n    });\n}\n\nexport async function serveMcpRPC(req, res) {\n    const body = req.body;\n    if (!body || typeof body !== \"object\") {\n        res.json({\n            jsonrpc: \"2.0\",\n            id: null,\n            error: { code: -32700, message: \"Parse Error\" },\n        });\n        Log.error(\"MCPServer\", { error: \"Invalid JSON\" });\n        return;\n    }\n    // check jsonrpc 2.0\n    if (body.jsonrpc !== \"2.0\") {\n        res.json({\n            jsonrpc: \"2.0\",\n            id: body.id || null,\n            error: {\n                code: -32600,\n                message: \"Invalid Request, only JSON-RPC 2.0 supported\",\n            },\n        });\n        Log.error(\"MCPServer\", { error: \"Invalid JSON-RPC version\", body });\n        return;\n    }\n    const method = body.method;\n    if (!method || typeof method !== \"string\") {\n        res.json({\n            jsonrpc: \"2.0\",\n            id: body.id || null,\n            error: {\n                code: -32600,\n                message: \"Invalid Request, method required\",\n            },\n        });\n        Log.error(\"MCPServer\", { error: \"Method not specified\", body });\n        return;\n    }\n    const id = body.id;\n    if (!id || (typeof id !== \"string\" && typeof id !== \"number\")) {\n        if (\n            ![\n                \"initialize\",\n                \"notifications/initialized\",\n                \"notifications/cancelled\",\n            ].includes(method)\n        ) {\n            res.json({\n                jsonrpc: \"2.0\",\n                id: null,\n                error: {\n                    code: -32600,\n                    message: \"Invalid Request, id required\",\n                },\n            });\n            Log.error(\"MCPServer\", { error: \"ID not specified\", body });\n            return;\n        }\n    }\n    if (!PluginHttpMCP[method]) {\n        res.json({\n            jsonrpc: \"2.0\",\n            id,\n            error: { code: -32601, message: \"Method not found\" },\n        });\n        Log.error(\"MCPServer\", { error: \"Method not found\", method, body });\n        return;\n    }\n    const params = body.params || {};\n    try {\n        const result = await PluginHttpMCP[method](params);\n        const json = { jsonrpc: \"2.0\", id, result };\n        Log.info(\"MCPServer.call\", { method, params, json });\n        res.json(json);\n    } catch (e) {\n        Log.error(\"MCPServer.call\", {\n            method,\n            params,\n            error: e + \"\",\n        });\n        res.json({\n            jsonrpc: \"2.0\",\n            id,\n            error: { code: -32000, message: e + \"\" },\n        });\n    }\n}\n\nexport const PluginHttpMCP = {\n    initialize: async (params: Record<string, any>) => {\n        return {\n            protocolVersion: \"2024-11-05\",\n            capabilities: {\n                tools: {\n                    listChanged: false,\n                },\n            },\n            serverInfo: {\n                name: \"FocusAny MCP Server\",\n                version: \"1.0.0\",\n            },\n        };\n    },\n    \"notifications/initialized\": async (params: Record<string, any>) => {\n        return {};\n    },\n    \"notifications/cancelled\": async (params: Record<string, any>) => {\n        return {};\n    },\n    \"tools/list\": async (params: Record<string, any>) => {\n        const tools: MCPToolsRecord[] = [];\n        const plugins = await ManagerPlugin.list();\n        for (const plugin of plugins) {\n            if (plugin.mcp) {\n                if (plugin.mcp.tools && Array.isArray(plugin.mcp.tools)) {\n                    for (const tool of plugin.mcp.tools) {\n                        tools.push({\n                            ...tool,\n                            name: `${plugin.name}-${tool.name}`,\n                        });\n                    }\n                }\n            }\n        }\n        return {\n            tools,\n        };\n    },\n    \"tools/call\": async (params: Record<string, any>) => {\n        const { name, arguments: args } = params;\n        const pcs = name.split(\"-\");\n        if (pcs.length < 2) {\n            throw new Error(\"Invalid tool name\");\n        }\n        const pluginName = pcs.shift()!;\n        const toolName = pcs.join(\"-\");\n        const plugin = await ManagerPlugin.get(pluginName);\n        if (!plugin) {\n            throw new Error(\"Plugin not found\");\n        }\n        const result: any = await ManagerBackend.run(\n            plugin,\n            \"mcpTool\",\n            toolName,\n            args || {},\n            { rejectIfError: true },\n        );\n        if (!result) {\n            PluginLog.error(\n                plugin.name,\n                `MCP.Tool.NoResult`,\n                {\n                    toolName,\n                    args,\n                },\n                true,\n            );\n            throw new Error(\"No result from tool\");\n        }\n        if (result.content && Array.isArray(result.content)) {\n            result.content = result.content.map((item) => {\n                if (item.type === \"image\") {\n                    // remove prefix data:image/png;base64,iVBORw\n                    if (item.data && item.data.startsWith(\"data:image\")) {\n                        const idx = item.data.indexOf(\"base64,\");\n                        if (idx > 0) {\n                            item.data = item.data.substring(idx + 7);\n                        }\n                    }\n                }\n                return item;\n            });\n        }\n        return result;\n    },\n};\n\nsetTimeout(async () => {\n    // PluginHttpMCP['tools/call']({\n    //     name: 'BasicExample.basic-example-weather',\n    //     arguments: {city: 'Beijing'},\n    // })\n    // const result = await PluginHttpMCP['tools/call']({\n    //     name: 'FabricEditor.fileToPng',\n    //     arguments: {path: '/Users/mz/Downloads/NewFile.FabricEditor.fad'},\n    // })\n    // Log.info('MCPServer.Test', {result});\n}, 5000);\n"
  },
  {
    "path": "electron/mapi/manager/plugin/index.ts",
    "content": "import {\n    ActionMatch,\n    ActionMatchKey,\n    ActionMatchRegex,\n    ActionMatchText,\n    ActionMatchTypeEnum,\n    ActionRecord,\n    ActionTypeEnum,\n    PluginEnv,\n    PluginRecord,\n    PluginType,\n} from \"../../../../src/types/Manager\";\nimport { Files } from \"../../file/main\";\nimport {\n    preloadDefault,\n    preloadPluginDefault,\n    rendererDistPath,\n    rendererIsUrl,\n} from \"../../../lib/env-main\";\nimport { join } from \"node:path\";\nimport { KVDBMain } from \"../../kvdb/main\";\nimport { CommonConfig } from \"../../../config/common\";\nimport {\n    MemoryCacheUtil,\n    StrUtil,\n    UIUtil,\n    VersionUtil,\n} from \"../../../lib/util\";\nimport { MiscMain } from \"../../misc/main\";\nimport { platformName } from \"../../../lib/env\";\nimport { AppConfig } from \"../../../../src/config\";\nimport { WindowConfig } from \"../../../config/window\";\nimport { AppsMain } from \"../../app/main\";\nimport { ManagerConfig } from \"../config/config\";\nimport { ManagerBackend } from \"../backend\";\nimport { session } from \"electron\";\nimport { PluginHttp } from \"./http\";\n\ntype PluginInfo = {\n    type: PluginType;\n    name: string;\n    version: string;\n    root: string;\n    config: PluginRecord;\n};\n\nexport const ManagerPlugin = {\n    installingMap: {} as {\n        [name: string]: {\n            version: string;\n            startTime: number;\n        };\n    },\n    async clearCache() {\n        MemoryCacheUtil.forget(\"Plugins\");\n        MemoryCacheUtil.forget(\"PluginActions\");\n    },\n    async getInfo(plugin: PluginRecord) {\n        // nodeIntegration\n        let nodeIntegration = false;\n        if (plugin.type === PluginType.SYSTEM) {\n            nodeIntegration = true;\n        } else if (plugin.setting && plugin.setting.nodeIntegration) {\n            nodeIntegration = true;\n        }\n        // preloadBase\n        let preloadBase = preloadPluginDefault;\n        if (plugin.setting && plugin.setting.preloadBase) {\n            preloadBase = plugin.setting.preloadBase;\n            if (preloadBase === \"<system>\") {\n                preloadBase = preloadDefault;\n            }\n        }\n        // preload\n        let preload = plugin.preload || null;\n        if (preload) {\n            if (preload === \"<system>\") {\n                preload = preloadDefault;\n            } else {\n                preload = join(plugin.runtime?.root, preload);\n            }\n        }\n        if (preload && preloadBase === preload) {\n            preload = null;\n        }\n        // main && mainView\n        let main = plugin.main || null;\n        if (main && plugin.setting?.httpEntry) {\n            main = await PluginHttp.url(plugin.name, main);\n        }\n        if (!main) {\n            main = rendererDistPath(\"static/pluginEmpty.html\");\n        }\n        let mainView = plugin.mainView || null;\n        if (!mainView) {\n            mainView = main;\n        }\n        if (mainView && plugin.setting?.httpEntry) {\n            mainView = await PluginHttp.url(plugin.name, mainView);\n        }\n        if (plugin.runtime?.root) {\n            if (!rendererIsUrl(main)) {\n                main = join(plugin.runtime?.root, main);\n            }\n        } else if (main.includes(\"<root>\")) {\n            main = main.replace(\"<root>/\", \"\");\n            main = rendererDistPath(main);\n        }\n        if (plugin.runtime?.root) {\n            if (!rendererIsUrl(mainView)) {\n                mainView = join(plugin.runtime?.root, mainView);\n            }\n        } else if (mainView.includes(\"<root>\")) {\n            mainView = mainView.replace(\"<root>/\", \"\");\n            mainView = rendererDistPath(mainView);\n        }\n        if (!rendererIsUrl(mainView)) {\n            mainView = `file://${mainView}`;\n        }\n\n        // auto detach\n        let autoDetach = false;\n        if (plugin.setting && plugin.setting.autoDetach) {\n            autoDetach = true;\n        }\n        if (\n            !autoDetach &&\n            plugin.runtime.config &&\n            plugin.runtime.config.autoDetach\n        ) {\n            autoDetach = true;\n        }\n        // width & height\n        let width = WindowConfig.pluginWidth;\n        let height = WindowConfig.pluginHeight;\n        if (plugin.setting) {\n            const display = AppsMain.getCurrentScreenDisplay();\n            if (plugin.setting.width) {\n                width = UIUtil.sizeToPx(\n                    plugin.setting.width + \"\",\n                    display.workArea.width,\n                );\n                autoDetach = true;\n            }\n            if (plugin.setting.height) {\n                height = UIUtil.sizeToPx(\n                    plugin.setting.height + \"\",\n                    display.workArea.height,\n                );\n                autoDetach = true;\n            }\n        }\n        // singleton\n        let singleton = true;\n        if (plugin.setting && \"singleton\" in plugin.setting) {\n            singleton = false;\n        }\n        // zoom\n        let zoom = 100;\n        if (plugin.setting && plugin.setting.zoom) {\n            zoom = plugin.setting.zoom;\n        }\n        if (plugin.runtime.config && plugin.runtime.config.zoom) {\n            zoom = plugin.runtime.config.zoom;\n        }\n        return {\n            nodeIntegration,\n            preloadBase,\n            preload,\n            main,\n            mainView,\n            width,\n            height,\n            autoDetach,\n            singleton,\n            zoom,\n        };\n    },\n    normalAction(action: ActionRecord, plugin: PluginRecord) {\n        const matches: ActionMatch[] = [];\n        for (let m of action.matches) {\n            if (typeof m === \"string\") {\n                m = {\n                    type: ActionMatchTypeEnum.TEXT,\n                    text: m,\n                } as any;\n            }\n            if (!m.name) {\n                switch (m.type) {\n                    case ActionMatchTypeEnum.TEXT:\n                        m.name = (m as ActionMatchText).text;\n                        break;\n                    case ActionMatchTypeEnum.KEY:\n                        m.name = (m as ActionMatchKey).key;\n                        break;\n                    case ActionMatchTypeEnum.REGEX:\n                        m.name = (m as ActionMatchRegex).regex;\n                        break;\n                    case ActionMatchTypeEnum.FILE:\n                    case ActionMatchTypeEnum.IMAGE:\n                    case ActionMatchTypeEnum.WINDOW:\n                        m.name = StrUtil.hashCode(JSON.stringify(m));\n                        break;\n                }\n            }\n            matches.push(m);\n        }\n        if (!(\"trackHistory\" in action)) {\n            action.trackHistory = true;\n        }\n        const normalAction = {\n            fullName: `${plugin.name}/${action.name}`,\n            pluginName: plugin.name,\n            name: action.name,\n            title: action.title || plugin.title,\n            icon: action.icon || plugin.logo,\n            trackHistory: action.trackHistory,\n            type: action.type || ActionTypeEnum.WEB,\n            pluginType: plugin.type,\n            matches: matches,\n            data: action.data || {},\n            platform: action.platform || [\"win\", \"osx\", \"linux\"],\n        } as ActionRecord;\n        if (plugin.runtime.root) {\n            if (normalAction.icon && !normalAction.icon.startsWith(\"file://\")) {\n                normalAction.icon = `file://${plugin.runtime.root}/${normalAction.icon}`;\n            }\n        }\n        return normalAction;\n    },\n    async initIfNeed(\n        plugin: PluginRecord,\n        option: {\n            type: PluginType;\n            root?: string;\n            configJson?: any;\n        },\n    ): Promise<PluginRecord> {\n        option = Object.assign(\n            {\n                type: null,\n            },\n            option,\n        );\n\n        if (!option.type) {\n            throw \"PluginTypeError\";\n        }\n\n        // console.log('ManagerPlugin.init', plugin.name, !plugin.runtime)\n\n        if (plugin.runtime) {\n            return plugin;\n        }\n\n        plugin.platform = plugin.platform || [\"win\", \"osx\", \"linux\"];\n        plugin.versionRequire = plugin.versionRequire || \"*\";\n        plugin.editionRequire = plugin.editionRequire || [\"open\", \"pro\"];\n\n        plugin.logo = plugin.logo || null;\n        plugin.main = plugin.main || null;\n        plugin.mainView = plugin.mainView || plugin.main;\n        plugin.preload = plugin.preload || null;\n        plugin.author = plugin.author || null;\n        plugin.homepage = plugin.homepage || null;\n\n        if (!plugin.mcp) {\n            plugin.mcp = {};\n        }\n        if (!plugin.mcp.tools) {\n            plugin.mcp.tools = [];\n        }\n\n        plugin.setting = Object.assign(\n            {\n                remoteWebCacheEnable: false,\n                httpEntry: false,\n                moreMenu: [],\n            },\n            plugin.setting || {},\n        );\n\n        plugin.development = Object.assign(\n            {\n                showDevTools: false,\n                showCodeDevTools: false,\n                keepCodeDevTools: false,\n            },\n            plugin.development,\n        );\n\n        plugin.type = option.type;\n        plugin.env = PluginEnv.PROD;\n\n        plugin.runtime = {\n            root: option.root,\n            config: await ManagerConfig.getPluginConfig(plugin.name),\n        };\n\n        if (plugin.runtime.root) {\n            if (plugin.logo && !plugin.logo.startsWith(\"file://\")) {\n                plugin.logo = `file://${plugin.runtime.root}/${plugin.logo}`;\n            }\n        }\n\n        for (let aIndex = 0; aIndex < plugin.actions.length; aIndex++) {\n            const a = this.normalAction(plugin.actions[aIndex], plugin);\n            if (!a.platform.includes(platformName())) {\n                continue;\n            }\n            plugin.actions[aIndex] = a;\n        }\n\n        const configJson = option.configJson || null;\n        if (configJson) {\n            if (configJson[\"development\"]) {\n                plugin.env = PluginEnv.DEV;\n                if (configJson[\"development\"].env) {\n                    plugin.env = configJson[\"development\"].env as any;\n                }\n                if (PluginEnv.DEV === plugin.env) {\n                    if (configJson[\"development\"].main) {\n                        plugin.main = configJson[\"development\"].main;\n                    }\n                    if (configJson[\"development\"].mainView) {\n                        plugin.mainView = configJson[\"development\"].mainView;\n                    }\n                }\n            }\n        }\n\n        return plugin;\n    },\n    async configCheck(config: any) {\n        if (!config) {\n            throw `PluginFormatError:-1`;\n        }\n        if (!config.name || !config.version) {\n            throw `PluginFormatError:-2`;\n        }\n        const existsP = await this.get(config.name);\n        if (existsP) {\n            throw `PluginAlreadyExists : ${config.name}`;\n        }\n        if (!config.platform) {\n            config.platform = [\"win\", \"osx\", \"linux\"];\n        }\n        if (!config.platform.includes(platformName())) {\n            throw `PluginNotSupportPlatform : ${config.name}`;\n        }\n        if (!config.versionRequire) {\n            config.versionRequire = \"*\";\n        }\n        if (!VersionUtil.match(AppConfig.version, config.versionRequire)) {\n            throw `PluginVersionNotMatch:-2:${config.name}`;\n        }\n        if (!config.editionRequire) {\n            config.editionRequire = [\"open\", \"pro\"];\n        }\n        if (!config.editionRequire.includes(\"open\")) {\n            throw `PluginEditionNotMatch:-1:${config.name}`;\n        }\n    },\n    async parsePackage(file: string, option?: {}) {\n        option = Object.assign({}, option);\n        if (!file.endsWith(\".zip\")) {\n            throw `PluginFormatError:-3`;\n        }\n        let config = null;\n        try {\n            config = await MiscMain.getZipFileContent(file, \"config.json\");\n        } catch (e) {\n            throw `PluginFormatError:-4`;\n        }\n        if (!config) {\n            throw `PluginFormatError:-5`;\n        }\n        try {\n            config = JSON.parse(config as string);\n        } catch (e) {\n            throw `PluginFormatError:-6`;\n        }\n        if (!config) {\n            throw `PluginFormatError:-7`;\n        }\n        if (!config.name || !config.version) {\n            throw `PluginFormatError:-8`;\n        }\n        const target = await Files.fullPath(`plugin/${config.name}`);\n        return {\n            name: config.name,\n            version: config.version,\n            target,\n        };\n    },\n    async installFromFileOrDir(fileOrPath: string, type?: PluginType) {\n        let guessType = type || PluginType.DIR;\n        if (\n            !(await Files.isDirectory(fileOrPath, {\n                isDataPath: false,\n            }))\n        ) {\n            if (fileOrPath.endsWith(\"config.json\")) {\n                fileOrPath = fileOrPath.replace(/[\\/\\\\]config.json$/, \"\");\n            } else {\n                guessType = PluginType.ZIP;\n                const { name, version, target } =\n                    await this.parsePackage(fileOrPath);\n                const plugin = await ManagerPlugin.get(name);\n                if (\n                    await Files.exists(target, {\n                        isDataPath: false,\n                    })\n                ) {\n                    if (!plugin) {\n                        await Files.deletes(target, {\n                            isDataPath: false,\n                        });\n                    }\n                }\n                try {\n                    await MiscMain.unzip(fileOrPath, target);\n                    fileOrPath = target;\n                } catch (e) {\n                    throw \"PluginInstallError\";\n                }\n            }\n        }\n        return await this.install(fileOrPath, type || guessType);\n    },\n    async install(root: string, type: PluginType) {\n        const p = await this._readPluginInfo(root);\n        if (!p) {\n            throw `PluginNotValid : ${root}`;\n        }\n        const existsP = await this.get(p.name);\n        if (existsP) {\n            throw `PluginAlreadyExists : ${p.name}`;\n        }\n        const plugin = await this.initIfNeed(p, {\n            type,\n            root,\n            configJson: p,\n        });\n        if (!plugin.platform.includes(platformName())) {\n            throw `PluginNotSupportPlatform : ${plugin.name}`;\n        }\n        if (!VersionUtil.match(AppConfig.version, plugin.versionRequire)) {\n            throw `PluginVersionNotMatch:-1:${plugin.name}`;\n        }\n        if (!plugin.editionRequire.includes(\"open\")) {\n            throw `PluginEditionNotMatch:-2:${plugin.name}`;\n        }\n        const runtime = plugin.runtime;\n        delete plugin.runtime;\n        const info: PluginInfo = {\n            type,\n            version: plugin.version,\n            name: plugin.name,\n            root,\n            config: plugin,\n        };\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: `${CommonConfig.dbPluginIdPrefix}/${info.name}`,\n            ...info,\n        });\n        await this.clearCache();\n        setTimeout(async () => {\n            plugin.runtime = runtime;\n            await ManagerBackend.run(plugin, \"hook\", \"installed\", {});\n        }, 1000);\n    },\n    async refreshInstall(name: string) {\n        const doc = await KVDBMain.get(\n            CommonConfig.dbSystem,\n            `${CommonConfig.dbPluginIdPrefix}/${name}`,\n        );\n        if (!doc) {\n            throw `PluginNotExists : ${name}`;\n        }\n        const pluginInfo: PluginInfo = doc as any;\n        const root = pluginInfo.root;\n        const p = await this._readPluginInfo(root);\n        // console.log('refreshInstall', JSON.stringify({name, root, p}, null, 2))\n        if (!p) {\n            throw `PluginNotValid : ${root}`;\n        }\n        const plugin = await this.initIfNeed(p, {\n            type: pluginInfo.type,\n            root,\n            configJson: p,\n        });\n        const runtime = plugin.runtime;\n        delete plugin.runtime;\n        const info: PluginInfo = {\n            type: pluginInfo.type,\n            version: plugin.version,\n            name: plugin.name,\n            root,\n            config: plugin,\n        };\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: `${CommonConfig.dbPluginIdPrefix}/${info.name}`,\n            ...info,\n        });\n        await this.clearCache();\n        setTimeout(async () => {\n            plugin.runtime = runtime;\n            await ManagerBackend.run(plugin, \"hook\", \"installed\", {});\n        }, 1000);\n    },\n    async uninstall(name: string) {\n        const plugin = await this.get(name);\n        if (!plugin) {\n            throw `PluginNotExists:-1:${name}`;\n        }\n        const pi = await KVDBMain.get(\n            CommonConfig.dbSystem,\n            `${CommonConfig.dbPluginIdPrefix}/${name}`,\n        );\n        if (!pi) {\n            throw `PluginNotExists:-2:${name}`;\n        }\n        const info: PluginInfo = pi as any;\n        if (!info.name || !info.version || !info.type || !info.config) {\n            throw `PluginNotExists:-3:${name}`;\n        }\n        await ManagerBackend.run(plugin, \"hook\", \"beforeUninstall\", {});\n        if (info.type === PluginType.STORE || info.type === PluginType.ZIP) {\n            if (info.root) {\n                await Files.deletes(info.root, {\n                    isDataPath: false,\n                });\n            }\n        }\n        await KVDBMain.remove(CommonConfig.dbSystem, pi);\n        await ManagerConfig.clearCustomAction(name);\n        await this.clearCache();\n        await this.clearViewSession(plugin);\n    },\n    async getPluginInstalledVersion(name: string) {\n        const plugin = await this.get(name);\n        if (!plugin) {\n            return null;\n        }\n        return plugin.version;\n    },\n    async isPluginInstalling(name: string) {},\n    async list(): Promise<PluginRecord[]> {\n        const plugins = await MemoryCacheUtil.remember<PluginRecord[]>(\n            \"Plugins\",\n            async () => {\n                // await this.install(`${process.cwd()}/plugin-examples/plugin-example`, 'system')\n                let plugins: PluginRecord[] = [];\n                const pluginInfos = await KVDBMain.allDocs(\n                    CommonConfig.dbSystem,\n                    `${CommonConfig.dbPluginIdPrefix}/`,\n                );\n                for (const pi of pluginInfos) {\n                    const info: PluginInfo = pi as any;\n                    if (\n                        !info.name ||\n                        !info.version ||\n                        !info.type ||\n                        !info.config\n                    ) {\n                        await KVDBMain.remove(CommonConfig.dbSystem, pi);\n                        continue;\n                    }\n                    let configJson = null;\n                    if (info.type === PluginType.DIR) {\n                        configJson = await this._readPluginInfo(info.root);\n                        info.config = configJson;\n                        if (!info.config) {\n                            // 本地插件可能已经被删除\n                            await KVDBMain.remove(CommonConfig.dbSystem, pi);\n                            continue;\n                        }\n                    }\n                    plugins.push(\n                        await this.initIfNeed(info.config, {\n                            type: info.type,\n                            root: info.root,\n                            configJson,\n                        }),\n                    );\n                }\n                // console.log('plugins', JSON.stringify(plugins))\n                return plugins;\n            },\n        );\n        // 有开发选项并且是开发环境的插件，每次都重新读取 config\n        for (let pIndex = 0; pIndex < plugins.length; pIndex++) {\n            const p = plugins[pIndex];\n            if (\n                p.type === PluginType.DIR &&\n                p.env === \"dev\" &&\n                p.runtime.root\n            ) {\n                const configJson = await this._readPluginInfo(p.runtime.root);\n                plugins[pIndex] = await this.initIfNeed(p, {\n                    type: p.type,\n                    root: p.runtime.root,\n                    configJson,\n                });\n            }\n        }\n        return plugins;\n    },\n    async get(name: string) {\n        for (const p of await this.list()) {\n            if (p.name === name) {\n                return p;\n            }\n        }\n        return null;\n    },\n    async _readPluginInfo(root: string) {\n        root = root.replace(/[\\\\/]+$/, \"\");\n        const configPath = root + \"/config.json\";\n        const config = await Files.read(configPath, {\n            isDataPath: false,\n        });\n        if (!config) {\n            return null;\n        }\n        try {\n            let configJson = JSON.parse(config);\n            if (!configJson) {\n                return null;\n            }\n            return configJson;\n        } catch (e) {}\n        return null;\n    },\n    async listAction() {\n        return await MemoryCacheUtil.remember<ActionRecord[]>(\n            \"PluginActions\",\n            async () => {\n                let actions: ActionRecord[] = [];\n                const plugins = await this.list();\n                for (const p of plugins) {\n                    actions = actions.concat(p.actions);\n                }\n                return actions;\n            },\n        );\n    },\n    async getViewSession(plugin: PluginRecord, name: string = null) {\n        if (name) {\n            return session.fromPartition(\"<\" + plugin.name + `:${name}>`);\n        }\n        return session.fromPartition(\"<\" + plugin.name + \">\");\n    },\n    async clearViewSession(plugin: PluginRecord) {\n        const viewSession = await this.getViewSession(plugin);\n        if (viewSession) {\n            await viewSession.clearStorageData();\n        }\n    },\n    isDevelopmentCheck(\n        plugin: PluginRecord,\n        key: keyof NonNullable<PluginRecord[\"development\"]>,\n    ) {\n        if (!plugin.development || plugin.development.env !== PluginEnv.DEV) {\n            return false;\n        }\n        return !!plugin.development[key];\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/llm.ts",
    "content": "// @ts-ignore\nimport { Model, Provider } from \"../../../../src/module/Model/types\";\n// @ts-ignore\nimport {\n    getProviderLogo,\n    getProviderTitle,\n    SystemProviders,\n} from \"../../../../src/module/Model/providers\";\n// @ts-ignore\nimport { SystemModels } from \"../../../../src/module/Model/models\";\nimport StorageMain from \"../../storage/main\";\nimport User from \"../../user/main\";\nimport { AppConfig } from \"../../../../src/config\";\nimport { ModelProvider } from \"../../../../src/module/Model/provider/provider\";\n\nconst listProviders = async (): Promise<Provider[]> => {\n    const results: Provider[] = [];\n    for (const providerId in SystemProviders) {\n        const provider = SystemProviders[providerId];\n        results.push({\n            id: providerId,\n            type: \"openai\",\n            title: getProviderTitle(providerId),\n            logo: getProviderLogo(providerId),\n            isSystem: true,\n            apiUrl: provider.api.url,\n            websites: {\n                official: provider.websites?.official,\n                docs: provider.websites?.docs,\n                models: provider.websites?.models,\n            },\n            data: {\n                apiKey: \"\",\n                apiHost: \"\",\n                models: (SystemModels[providerId] || []).map((m) => {\n                    return {\n                        id: m.id,\n                        provider: providerId,\n                        name: m.name,\n                        group: m.group,\n                        types: [\"text\"],\n                        enabled: false,\n                    } as any;\n                }),\n                enabled: false,\n            },\n        });\n    }\n    const storageData = await StorageMain.read(\"models\", []);\n    let buildInProviderData: any = null;\n    if (storageData) {\n        if (storageData.userProviders) {\n            storageData.userProviders.forEach((provider) => {\n                results.unshift({\n                    id: provider.id,\n                    type: provider.type,\n                    title: provider.title,\n                    logo: null,\n                    isSystem: false,\n                    apiUrl: \"\",\n                    websites: {\n                        official: \"\",\n                        docs: \"\",\n                        models: \"\",\n                    },\n                    data: {\n                        apiKey: \"\",\n                        apiHost: \"\",\n                        models: [],\n                        enabled: false,\n                    },\n                });\n            });\n        }\n        if (storageData.providerData) {\n            buildInProviderData = storageData.providerData[\"buildIn\"] || null;\n            for (const providerId in storageData.providerData) {\n                const provider = results.find((p) => p.id === providerId);\n                if (provider) {\n                    provider.data.apiKey =\n                        storageData.providerData[providerId].apiKey || \"\";\n                    provider.data.apiHost =\n                        storageData.providerData[providerId].apiHost;\n                    (storageData.providerData[providerId].models || []).forEach(\n                        (model) => {\n                            const existingModel = provider.data.models.find(\n                                (m) => m.id === model.id,\n                            );\n                            if (existingModel) {\n                                existingModel.name = model.name;\n                                existingModel.group = model.group;\n                                existingModel.types = model.types;\n                                existingModel.enabled = model.enabled || false;\n                            } else {\n                                provider.data.models.push({\n                                    id: model.id,\n                                    provider: providerId,\n                                    name: model.name,\n                                    group: model.group,\n                                    types: [\"text\"],\n                                    enabled: model.enabled || false,\n                                    editable: true,\n                                });\n                            }\n                        },\n                    );\n                    provider.data.enabled =\n                        storageData.providerData[providerId].enabled || false;\n                }\n            }\n        }\n    }\n    const user = await User.get();\n    if (user.data && user.data.lmApi && user.data.lmApi.models) {\n        const lmApi = user.data.lmApi;\n        const models: Model[] = [];\n        for (const m of lmApi.models) {\n            models.push({\n                id: m,\n                provider: \"buildIn\",\n                name: m,\n                group: \"Default\",\n                types: [\"text\"],\n                enabled: true,\n                editable: false,\n            });\n        }\n        let enabled = true;\n        if (buildInProviderData && \"enabled\" in buildInProviderData) {\n            enabled = buildInProviderData.enabled;\n        }\n        results.unshift({\n            id: \"buildIn\",\n            type: \"openai\",\n            title: getProviderTitle(\"buildIn\"),\n            logo: getProviderLogo(\"buildIn\"),\n            isSystem: true,\n            apiUrl: lmApi.apiUrl,\n            websites: {\n                official: AppConfig.website,\n                docs: AppConfig.website,\n                models: AppConfig.website,\n            },\n            data: {\n                apiKey: lmApi.apiKey,\n                apiHost: \"\",\n                models: models,\n                enabled: enabled,\n            },\n        });\n    }\n    return results;\n};\n\nexport const listModels = async () => {\n    const providers = await listProviders();\n    const results: {\n        providerId: string;\n        providerLogo: string;\n        providerTitle: string;\n        modelId: string;\n        modelName: string;\n    }[] = [];\n    for (const provider of providers) {\n        if (!provider.data || !provider.data.enabled || !provider.data.models) {\n            continue;\n        }\n        for (const model of provider.data.models) {\n            if (model.enabled) {\n                results.push({\n                    providerId: provider.id,\n                    providerLogo: provider.logo || \"\",\n                    providerTitle: provider.title,\n                    modelId: model.id,\n                    modelName: model.name,\n                });\n            }\n        }\n    }\n    return results;\n};\n\nexport const modelChat = async (\n    providerId: string,\n    modelId: string,\n    message: string,\n): Promise<{\n    code: number;\n    msg: string;\n    data?: {\n        message: string;\n    };\n}> => {\n    const providers = await listProviders();\n    const provider = providers.find((p) => p.id === providerId);\n    if (!provider) {\n        throw new Error(`Provider not found: ${providerId}`);\n    }\n    const model = provider.data.models.find((m) => m.id === modelId);\n    if (!model || !model.enabled) {\n        throw new Error(`Model not found or not enabled: ${modelId}`);\n    }\n    const res = await ModelProvider.chat(message, {\n        type: provider.type,\n        modelId: model.id,\n        apiUrl: provider.apiUrl,\n        apiHost: provider.data.apiHost,\n        apiKey: provider.data.apiKey,\n    });\n    if (res.code) {\n        return {\n            code: -1,\n            msg: res.msg,\n        };\n    }\n    return {\n        code: 0,\n        msg: \"ok\",\n        data: {\n            message: res.data.content,\n        },\n    };\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/log.ts",
    "content": "import { t } from \"../../../config/lang\";\nimport { AppsMain } from \"../../app/main\";\nimport { Log } from \"../../log/main\";\n\nexport const PluginLog = {\n    name: (pluginName: string) => {\n        return `Plugin_${pluginName}`;\n    },\n    info: (pluginName: string, label: string, data: any) => {\n        const name = PluginLog.name(pluginName);\n        Log.appInfo(name, label, data);\n    },\n    error: (\n        pluginName: string,\n        label: string,\n        data: any,\n        toast: boolean = false,\n    ) => {\n        const name = PluginLog.name(pluginName);\n        Log.appError(name, label, data);\n        if (toast) {\n            AppsMain.toast(\n                t(\"plugin.errorLog\", {\n                    name: pluginName,\n                    error: label,\n                }),\n            ).then();\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/permission.ts",
    "content": "import {\n    PluginPermissionType,\n    PluginRecord,\n} from \"../../../../src/types/Manager\";\nimport { t } from \"../../../config/lang\";\nimport { AppsMain } from \"../../app/main\";\n\nexport const ManagerPluginPermission = {\n    checkPermit(\n        plugin: PluginRecord,\n        permission: PluginPermissionType,\n    ): boolean {\n        if (\n            plugin.permissions &&\n            plugin.permissions.length > 0 &&\n            plugin.permissions.includes(permission)\n        ) {\n            return true;\n        }\n        AppsMain.toast(t(\"plugin.noPermission\", { permission }), {\n            status: \"error\",\n        });\n        return false;\n    },\n    /**\n     * check if the plugin has permission for a specific type and typeData\n     * @param plugin\n     * @param type basic | event\n     * @param typeData\n     */\n    check(\n        plugin: PluginRecord,\n        type: \"basic\" | \"event\",\n        typeData: string,\n    ): boolean {\n        // console.log('ManagerPluginPermission.check', JSON.stringify(plugin, null, 2))\n        if (\"basic\" === type) {\n            return this.checkPermit(plugin, typeData as PluginPermissionType);\n        } else if (\"event\" === type) {\n            if (typeData === \"ClipboardChange\") {\n                return this.checkPermit(plugin, \"ClipboardManage\");\n            } else if ([\"UserChange\"].includes(typeData)) {\n                return true;\n            }\n        }\n        AppsMain.toast(\n            t(\"plugin.noPermission\", { permission: `${type}.${typeData}` }),\n            { status: \"error\" },\n        );\n        return false;\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/screenCapture.ts",
    "content": "import { exec, execFile } from \"child_process\";\nimport { clipboard, Notification } from \"electron\";\nimport { t } from \"../../../config/lang\";\nimport { extraResolve, isMac, isWin } from \"../../../lib/env\";\n\nconst forWindows = (cb: (image: string) => void) => {\n    const screenCaptureUrl = extraResolve(\"win/ScreenCapture.exe\");\n    const screen_window = execFile(screenCaptureUrl);\n    screen_window.on(\"exit\", (code) => {\n        if (code) {\n            const image = clipboard.readImage();\n            cb && cb(image.isEmpty() ? \"\" : image.toDataURL());\n        }\n    });\n};\n\nconst forMac = (cb: (image: string) => void) => {\n    exec(\"screencapture -i -r -c\", () => {\n        const image = clipboard.readImage();\n        cb && cb(image.isEmpty() ? \"\" : image.toDataURL());\n    });\n};\n\nconst forLinux = (cb: (image: string) => void) => {\n    const notify = new Notification({\n        title: t(\"system.screenshot\"),\n        body: t(\"plugin.screenshotHint\"),\n    });\n    notify.show();\n};\n\nexport const screenCapture = (cb: (image: string) => void) => {\n    clipboard.writeText(\"\");\n    if (isMac) {\n        forMac(cb);\n    } else if (isWin) {\n        forWindows(cb);\n    } else {\n        forLinux(cb);\n    }\n};\n"
  },
  {
    "path": "electron/mapi/manager/plugin/screenRecord.ts",
    "content": "import { spawn } from \"child_process\";\nimport { BrowserWindow, desktopCapturer, dialog, screen } from \"electron\";\nimport { t } from \"../../../config/lang\";\n\nlet isRecording = false;\nlet ffmpegProcess: any = null;\nlet recordingWindow: BrowserWindow | null = null;\n// let tray: Tray = (global as any).tray;\n\nconst screenRecord = async (): Promise<void> => {\n    if (isRecording) {\n        stopRecording();\n        return;\n    }\n\n    // 选择保存路径\n    const result = await dialog.showSaveDialog({\n        title: t(\"plugin.selectSavePath\"),\n        defaultPath: \"screen_record.mp4\",\n        filters: [{ name: \"MP4\", extensions: [\"mp4\"] }],\n    });\n\n    if (result.canceled) return;\n\n    const savePath = result.filePath;\n\n    // 获取屏幕源\n    const sources = await desktopCapturer.getSources({ types: [\"screen\"] });\n    if (sources.length === 0) return;\n\n    // 选择屏幕，假设第一个\n    const source = sources[0];\n    const displays = screen.getAllDisplays();\n    const display =\n        displays.find((d) => d.id === Number(source.display_id)) || displays[0];\n\n    // 选择区域\n    const bounds = await selectArea(display.bounds);\n    if (!bounds) return;\n\n    // 开始录制\n    startRecording(savePath, source.id, bounds, display);\n};\n\nconst selectArea = async (screenBounds: any): Promise<any> => {\n    return new Promise((resolve) => {\n        const win = new BrowserWindow({\n            x: screenBounds.x,\n            y: screenBounds.y,\n            width: screenBounds.width,\n            height: screenBounds.height,\n            frame: false,\n            transparent: true,\n            alwaysOnTop: true,\n            webPreferences: {\n                nodeIntegration: true,\n                contextIsolation: false,\n            },\n        });\n\n        win.loadURL(\n            `data:text/html;charset=utf-8,${encodeURIComponent(`\n<html>\n<body style=\"margin:0;padding:0;background:rgba(0,0,0,0.3);cursor:crosshair;\" onmousedown=\"start(event)\" onmousemove=\"move(event)\" onmouseup=\"end(event)\">\n<div id=\"rect\" style=\"position:absolute;border:2px solid red;display:none;\"></div>\n<script>\nlet startX, startY, isSelecting = false;\nfunction start(e) {\n  startX = e.clientX;\n  startY = e.clientY;\n  isSelecting = true;\n  document.getElementById('rect').style.display = 'block';\n}\nfunction move(e) {\n  if (!isSelecting) return;\n  const rect = document.getElementById('rect');\n  const x = Math.min(startX, e.clientX);\n  const y = Math.min(startY, e.clientY);\n  const w = Math.abs(e.clientX - startX);\n  const h = Math.abs(e.clientY - startY);\n  rect.style.left = x + 'px';\n  rect.style.top = y + 'px';\n  rect.style.width = w + 'px';\n  rect.style.height = h + 'px';\n}\nfunction end(e) {\n  if (!isSelecting) return;\n  isSelecting = false;\n  const x = Math.min(startX, e.clientX);\n  const y = Math.min(startY, e.clientY);\n  const w = Math.abs(e.clientX - startX);\n  const h = Math.abs(e.clientY - startY);\n  window.postMessage({type:'select', bounds:{x,y,width:w,height:h}}, '*');\n}\nwindow.addEventListener('message', (e) => {\n  if (e.data.type === 'select') {\n    console.log('SELECT:' + JSON.stringify(e.data.bounds));\n  }\n});\n</script>\n</body>\n</html>\n    `)}`,\n        );\n\n        win.webContents.on(\"console-message\", (event, level, message) => {\n            if (message.startsWith(\"SELECT:\")) {\n                const bounds = JSON.parse(message.substring(7));\n                win.close();\n                resolve(bounds);\n            }\n        });\n\n        win.on(\"closed\", () => resolve(null));\n    });\n};\n\nconst startRecording = (\n    savePath: string,\n    sourceId: string,\n    bounds: any,\n    display: any,\n) => {\n    isRecording = true;\n\n    // 创建录制指示器窗口\n    recordingWindow = new BrowserWindow({\n        x: display.bounds.x + bounds.x,\n        y: display.bounds.y + bounds.y,\n        width: bounds.width,\n        height: bounds.height,\n        frame: false,\n        transparent: true,\n        alwaysOnTop: true,\n        webPreferences: {\n            nodeIntegration: false,\n            contextIsolation: true,\n        },\n    });\n\n    recordingWindow.loadURL(\n        `data:text/html;charset=utf-8,${encodeURIComponent(`\n<html>\n<head>\n<style>\nbody {\n    margin: 0;\n    padding: 0;\n    background: transparent;\n    border: 2px solid red;\n    box-sizing: border-box;\n    width: 100%;\n    height: 100%;\n    animation: blink 1s infinite;\n}\n@keyframes blink {\n    0% { border-color: red; }\n    50% { border-color: transparent; }\n    100% { border-color: red; }\n}\n</style>\n</head>\n<body></body>\n</html>\n    `)}`,\n    );\n\n    // 改变tray\n    // if (tray) {\n    //     // 假设停止图标路径\n    //     const stopIconPath = path.join(__dirname, '../../../public/iconfont/stop.png'); // 需要实际图标\n    //     tray.setImage(stopIconPath);\n    //     tray.setToolTip('点击停止录制');\n    //     tray.removeAllListeners('click');\n    //     tray.on('click', () => stopRecording());\n    // }\n\n    // ffmpeg命令\n    const platform = process.platform;\n    let args: string[];\n    if (platform === \"darwin\") {\n        const screenIndex = parseInt(sourceId.split(\":\")[1]) + 1;\n        args = [\n            \"-f\",\n            \"avfoundation\",\n            \"-i\",\n            `${screenIndex}`,\n            \"-vf\",\n            `crop=${bounds.width}:${bounds.height}:${bounds.x}:${bounds.y}`,\n            \"-c:v\",\n            \"libx264\",\n            \"-preset\",\n            \"fast\",\n            \"-crf\",\n            \"22\",\n            \"-c:a\",\n            \"aac\",\n            savePath,\n        ];\n    } else if (platform === \"win32\") {\n        args = [\n            \"-f\",\n            \"gdigrab\",\n            \"-i\",\n            \"desktop\",\n            \"-vf\",\n            `crop=${bounds.width}:${bounds.height}:${bounds.x}:${bounds.y}`,\n            \"-c:v\",\n            \"libx264\",\n            \"-preset\",\n            \"fast\",\n            \"-crf\",\n            \"22\",\n            savePath,\n        ];\n    } else {\n        args = [\n            \"-f\",\n            \"x11grab\",\n            \"-i\",\n            \":0.0\",\n            \"-vf\",\n            `crop=${bounds.width}:${bounds.height}:${bounds.x}:${bounds.y}`,\n            \"-c:v\",\n            \"libx264\",\n            \"-preset\",\n            \"fast\",\n            \"-crf\",\n            \"22\",\n            savePath,\n        ];\n    }\n\n    ffmpegProcess = spawn(\"ffmpeg\", args);\n    ffmpegProcess.on(\"close\", () => {\n        stopRecording();\n    });\n};\n\nconst stopRecording = () => {\n    if (!isRecording) return;\n    isRecording = false;\n    if (ffmpegProcess) {\n        ffmpegProcess.kill(\"SIGINT\");\n        ffmpegProcess = null;\n    }\n\n    // 关闭录制指示器窗口\n    if (recordingWindow) {\n        recordingWindow.close();\n        recordingWindow = null;\n    }\n\n    // 恢复tray\n    // if (tray) {\n    //     const defaultIconPath = path.join(__dirname, '../../../public/static/tray/icon.png'); // 假设默认图标路径\n    //     tray.setImage(defaultIconPath);\n    //     tray.setToolTip('FocusAny');\n    //     tray.removeAllListeners('click');\n    //     // 恢复原始点击事件\n    //     tray.on('click', () => {\n    //         // 假设显示主界面\n    //     });\n    // }\n};\n\nexport { screenRecord };\n"
  },
  {
    "path": "electron/mapi/manager/plugin/sdk.ts",
    "content": "import { BrowserWindow, screen, shell } from \"electron\";\nimport os from \"os\";\nimport path from \"path\";\nimport { PluginRecord } from \"../../../../src/types/Manager\";\nimport { t } from \"../../../config/lang\";\nimport { EncodeUtil, FileUtil, StrUtil, TimeUtil } from \"../../../lib/util\";\nimport { PluginContext } from \"../type\";\nimport { ManagerPluginEvent } from \"./event\";\nimport { PluginLog } from \"./log\";\n\nexport const PluginSdkCreate = (plugin: PluginRecord) => {\n    const context = {\n        _window: null,\n        _plugin: plugin,\n    } as PluginContext;\n    const sdk = {\n        async isMacOs() {\n            return os.type() === \"Darwin\";\n        },\n        async isWindows() {\n            return os.type() === \"Windows_NT\";\n        },\n        async isLinux() {\n            return os.type() === \"Linux\";\n        },\n        async getPlatformArch() {\n            return ManagerPluginEvent.getPlatformArch(context, {});\n        },\n        async isMainWindowShown() {\n            return ManagerPluginEvent.isMainWindowShown(context, {});\n        },\n        async hideMainWindow() {\n            return ManagerPluginEvent.hideMainWindow(context, {});\n        },\n        async showMainWindow() {\n            return ManagerPluginEvent.showMainWindow(context, {});\n        },\n        async isFastPanelWindowShown() {\n            return ManagerPluginEvent.isFastPanelWindowShown(context, {});\n        },\n        async showFastPanelWindow() {\n            return ManagerPluginEvent.showFastPanelWindow(context, {});\n        },\n        async hideFastPanelWindow() {\n            return ManagerPluginEvent.hideFastPanelWindow(context, {});\n        },\n        async showOpenDialog() {\n            return ManagerPluginEvent.showOpenDialog(context, {});\n        },\n        async showSaveDialog() {\n            return ManagerPluginEvent.showSaveDialog(context, {});\n        },\n        async getPluginRoot() {\n            return plugin.runtime?.root;\n        },\n        async getPluginConfig() {\n            return ManagerPluginEvent.getPluginConfig(context, {});\n        },\n        async getPluginInfo() {\n            return ManagerPluginEvent.getPluginInfo(context, {});\n        },\n        async getPluginEnv() {\n            return ManagerPluginEvent.getPluginEnv(context, {});\n        },\n        async getPath(\n            name:\n                | \"home\"\n                | \"appData\"\n                | \"userData\"\n                | \"temp\"\n                | \"exe\"\n                | \"desktop\"\n                | \"documents\"\n                | \"downloads\"\n                | \"music\"\n                | \"pictures\"\n                | \"videos\"\n                | \"logs\",\n        ) {\n            return ManagerPluginEvent.getPath(context, { name });\n        },\n        async showToast(\n            body: string,\n            options?: {\n                duration?: number;\n                status?: \"info\" | \"success\" | \"error\";\n            },\n        ) {\n            ManagerPluginEvent.showToast(context, { body, options }).then();\n        },\n        async showNotification(body: string, clickActionName: string) {\n            return ManagerPluginEvent.showNotification(context, {\n                body,\n                clickActionName,\n            });\n        },\n        async showMessageBox(\n            message: string,\n            options: {\n                title?: string;\n                yes?: string;\n                no?: string;\n            },\n        ) {\n            return ManagerPluginEvent.showMessageBox(context, {\n                message,\n                ...options,\n            });\n        },\n        async copyImage(img: string) {\n            return ManagerPluginEvent.copyImage(context, { img });\n        },\n        async copyText(text: string) {\n            return ManagerPluginEvent.copyText(context, { text });\n        },\n        async copyFile(file: string) {\n            return ManagerPluginEvent.copyFile(context, { file });\n        },\n        async getClipboardText() {\n            return ManagerPluginEvent.getClipboardText(context, {});\n        },\n        async getClipboardImage() {\n            return ManagerPluginEvent.getClipboardImage(context, {});\n        },\n        async getClipboardFiles(): Promise<\n            {\n                name: string;\n                pathname: string;\n                isDirectory: boolean;\n                size: number;\n                lastModified: number;\n            }[]\n        > {\n            return (await ManagerPluginEvent.getClipboardFiles(\n                context,\n                {},\n            )) as any;\n        },\n        async listClipboardItems(option?: { limit?: number }): Promise<\n            {\n                type: \"file\" | \"image\" | \"text\";\n                timestamp: number;\n                files?: FileItem[];\n                image?: string;\n                text?: string;\n            }[]\n        > {\n            return ManagerPluginEvent.listClipboardItems(context, option || {});\n        },\n        async deleteClipboardItem(timestamp: number): Promise<void> {\n            return ManagerPluginEvent.deleteClipboardItem(context, {\n                timestamp,\n            });\n        },\n        async clearClipboardItems(): Promise<void> {\n            return ManagerPluginEvent.clearClipboardItems(context, {});\n        },\n        async shellOpenExternal(url: string) {\n            await shell.openExternal(url);\n        },\n        async shellOpenPath(path: string) {\n            await shell.openPath(path).then();\n        },\n        async shellShowItemInFolder(path: string) {\n            await ManagerPluginEvent.shellShowItemInFolder(context, { path });\n        },\n        async shellBeep() {\n            return ManagerPluginEvent.shellBeep(context, {});\n        },\n        async getFileIcon(path: string) {\n            return ManagerPluginEvent.getFileIcon(context, { path });\n        },\n        simulate: {\n            async keyboardTap(\n                key: string,\n                modifiers: (\"ctrl\" | \"shift\" | \"command\" | \"option\" | \"alt\")[],\n            ) {\n                await ManagerPluginEvent.simulateKeyboardTap(context, {\n                    key,\n                    modifiers,\n                });\n            },\n            async typeString(text: string) {\n                await ManagerPluginEvent.simulateTypeString(context, { text });\n            },\n            async mouseToggle(\n                type: \"down\" | \"up\",\n                button: \"left\" | \"right\" | \"middle\",\n            ) {\n                await ManagerPluginEvent.simulateMouseToggle(context, {\n                    type,\n                    button,\n                });\n            },\n            async mouseMove(x: number, y: number) {\n                await ManagerPluginEvent.simulateMouseMove(context, { x, y });\n            },\n            async mouseClick(\n                button: \"left\" | \"right\" | \"middle\",\n                double?: boolean,\n            ) {\n                await ManagerPluginEvent.simulateMouseClick(context, {\n                    button,\n                    double,\n                });\n            },\n        },\n        async getCursorScreenPoint() {\n            return screen.getCursorScreenPoint();\n        },\n        async getDisplayNearestPoint(point: { x: number; y: number }) {\n            return screen.getDisplayNearestPoint(point);\n        },\n        // sendTo\n        async createBrowserWindow(url: string, options: any, callback: any) {\n            const pluginRoot = await this.getPluginRoot();\n            url = path.join(pluginRoot, url);\n            let preloadPath = null;\n            if (options.webPreferences && options.webPreferences.preload) {\n                preloadPath = path.join(\n                    pluginRoot,\n                    options.webPreferences.preload,\n                );\n            }\n            if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) {\n                // do nothing\n            } else {\n                url = `file://${url}`;\n            }\n            options = options || {};\n            let win = new BrowserWindow({\n                useContentSize: true,\n                resizable: true,\n                title: options.title || t(\"plugin.newWindow\"),\n                show: true,\n                backgroundColor: \"#fff\",\n                ...options,\n                webPreferences: {\n                    webSecurity: false,\n                    backgroundThrottling: false,\n                    contextIsolation: false,\n                    webviewTag: true,\n                    nodeIntegration: true,\n                    spellcheck: false,\n                    partition: null,\n                    ...(options.webPreferences || {}),\n                    preload: preloadPath,\n                },\n            });\n            win.loadURL(url);\n            win.on(\"closed\", () => {\n                win = undefined;\n            });\n            win.once(\"ready-to-show\", () => {\n                win.show();\n            });\n            win.webContents.on(\"dom-ready\", () => {\n                callback && callback();\n            });\n            return win;\n        },\n        async screenCapture(cb: Function) {\n            context[\"_screenCaptureCallback\"] = (data: { image: string }) => {\n                cb && cb(data.image);\n            };\n            return ManagerPluginEvent.screenCapture(context, { cb });\n        },\n        getNativeId() {\n            return ManagerPluginEvent.getNativeId(context, {});\n        },\n        getAppVersion() {\n            return ManagerPluginEvent.getAppVersion(context, {});\n        },\n        async isDarkColors() {\n            return ManagerPluginEvent.isDarkColors(context, {});\n        },\n        async redirect(keywordsOrAction: string | string[], payload: any) {\n            return ManagerPluginEvent.redirect(context, {\n                keywordsOrAction,\n                payload,\n            });\n        },\n        async getActions(names?: string[]) {\n            return ManagerPluginEvent.getActions(context, { names });\n        },\n        async setAction(action: string) {\n            return ManagerPluginEvent.setAction(context, { action });\n        },\n        async removeAction(name: string) {\n            return ManagerPluginEvent.removeAction(context, { name });\n        },\n        async sendBackendEvent(\n            event: string,\n            data?: any,\n            option?: {\n                timeout: number;\n            },\n        ): Promise<any> {\n            throw new Error(\"Only can be called in plugin web\");\n        },\n        registerCallPage(\n            type: string,\n            callback: (\n                resolve: (data: any) => void,\n                reject: (error: string) => void,\n                data: any,\n            ) => void,\n            option?: {\n                timeout?: number;\n            },\n        ) {\n            throw new Error(\"Only can be called in plugin web\");\n        },\n        callPage(\n            type: string,\n            data?: any,\n            option?: CallPageOption,\n        ): Promise<any> {\n            return ManagerPluginEvent.callPage(context, { type, data, option });\n        },\n        setRemoteWebRuntime(info: {\n            userAgent: string;\n            urlMap: Record<string, string>;\n            types: string[];\n            domains: string[];\n            blocks: string[];\n        }): Promise<undefined> {\n            throw new Error(\"Only can be called in plugin web\");\n        },\n        async llmListModels(): Promise<\n            {\n                providerId: string;\n                providerLogo: string;\n                providerTitle: string;\n                modelId: string;\n                modelName: string;\n            }[]\n        > {\n            return ManagerPluginEvent.llmListModels(context, {});\n        },\n\n        async llmChat(callInfo: {\n            providerId: string;\n            modelId: string;\n            message: string;\n        }): Promise<{\n            code: number;\n            msg: string;\n            data?: {\n                message: string;\n            };\n        }> {\n            return ManagerPluginEvent.llmChat(context, { callInfo });\n        },\n\n        logInfo(label: string, data?: any): void {\n            ManagerPluginEvent.logInfo(context, { label, logData: data });\n        },\n\n        logError(label: string, data?: any): void {\n            ManagerPluginEvent.logError(context, { label, logData: data });\n        },\n\n        async logPath(): Promise<string> {\n            return ManagerPluginEvent.logPath(context, {});\n        },\n\n        logShow(): void {\n            ManagerPluginEvent.logShow(context, {});\n        },\n\n        async addLaunch(\n            keyword: string,\n            name: string,\n            hotkey: HotkeyType,\n        ): Promise<void> {\n            return ManagerPluginEvent.addLaunch(context, {\n                keyword,\n                name,\n                hotkey,\n            });\n        },\n\n        async removeLaunch(keyword: string): Promise<void> {\n            return ManagerPluginEvent.removeLaunch(context, { keyword });\n        },\n\n        async activateLatestWindow(): Promise<void> {\n            return ManagerPluginEvent.activateLatestWindow(context, {});\n        },\n\n        async getUser(): Promise<{\n            avatar: string;\n            nickname: string;\n            vipFlag: string;\n            deviceCode: string;\n        } | null> {\n            return ManagerPluginEvent.getUser(context, {});\n        },\n        async getUserAccessToken(): Promise<{\n            token: string;\n            expireAt: number;\n        }> {\n            return ManagerPluginEvent.getUserAccessToken(context, {});\n        },\n        file: {\n            async exists(path: string): Promise<boolean> {\n                return ManagerPluginEvent.fileExists(context, { path });\n            },\n            async read(path: string): Promise<string> {\n                return ManagerPluginEvent.fileRead(context, { path });\n            },\n            async write(path: string, data: string): Promise<void> {\n                return ManagerPluginEvent.fileWrite(context, { path, data });\n            },\n            async remove(path: string): Promise<void> {\n                return ManagerPluginEvent.fileRemove(context, { path });\n            },\n            async ext(path: string): Promise<string> {\n                return ManagerPluginEvent.fileExt(context, { path });\n            },\n            async writeTemp(\n                ext: string,\n                data: string | Uint8Array,\n                option?: {\n                    isBase64?: boolean;\n                },\n            ): Promise<string> {\n                return ManagerPluginEvent.fileWriteTemp(context, {\n                    ext,\n                    data,\n                    option,\n                });\n            },\n        },\n        db: {\n            async put(doc: { _id: string; data: any; _rev?: string }) {\n                return ManagerPluginEvent.dbPut(context, { doc });\n            },\n            async get(id: string) {\n                return ManagerPluginEvent.dbGet(context, { id });\n            },\n            async remove(\n                doc:\n                    | {\n                          _id: string;\n                      }\n                    | string,\n            ) {\n                return ManagerPluginEvent.dbRemove(context, { doc });\n            },\n            async bulkDocs(\n                docs: {\n                    _id: string;\n                    data: any;\n                    _rev?: string;\n                }[],\n            ) {\n                return ManagerPluginEvent.dbBulkDocs(context, { docs });\n            },\n            async allDocs(key: string | string[]) {\n                return ManagerPluginEvent.dbAllDocs(context, { key });\n            },\n            async postAttachment(\n                docId: string,\n                attachment: Buffer | Uint8Array,\n                type: string,\n            ) {\n                return ManagerPluginEvent.dbPostAttachment(context, {\n                    docId,\n                    attachment,\n                    type,\n                });\n            },\n            async getAttachment(docId: string) {\n                return ManagerPluginEvent.dbGetAttachment(context, { docId });\n            },\n            async getAttachmentType(docId: string) {\n                return ManagerPluginEvent.dbGetAttachmentType(context, {\n                    docId,\n                });\n            },\n        },\n        dbStorage: {\n            async setItem(key: string, value: any) {\n                return ManagerPluginEvent.dbStorageSetItem(context, {\n                    key,\n                    value,\n                });\n            },\n            async getItem(key: string) {\n                return ManagerPluginEvent.dbStorageGetItem(context, { key });\n            },\n            async removeItem(key: string) {\n                return ManagerPluginEvent.dbStorageRemoveItem(context, { key });\n            },\n        },\n        util: {\n            randomString(length: number) {\n                return StrUtil.randomString(length);\n            },\n            bufferToBase64(buffer: Buffer) {\n                return FileUtil.bufferToBase64(buffer);\n            },\n            datetimeString() {\n                return TimeUtil.datetimeString();\n            },\n            base64Encode(data: any) {\n                return EncodeUtil.base64Encode(data);\n            },\n            base64Decode(data: string) {\n                return EncodeUtil.base64Decode(data);\n            },\n            md5(data: string) {\n                return EncodeUtil.md5(data);\n            },\n        },\n    };\n\n    const createDeepProxy = (target: any, cache = new WeakMap()) => {\n        if (typeof target !== \"object\" || target === null) {\n            return target;\n        }\n        if (cache.has(target)) {\n            return cache.get(target);\n        }\n        const proxy = new Proxy(target, {\n            get(obj, prop) {\n                const value = Reflect.get(obj, prop);\n                if (typeof value === \"function\") {\n                    return async function (...args: any[]) {\n                        try {\n                            return await Promise.resolve(\n                                value.apply(obj, args),\n                            );\n                        } catch (error) {\n                            PluginLog.error(\n                                plugin.name,\n                                `SDK-${prop.toString()}`,\n                                {\n                                    error: error + \"\",\n                                },\n                            );\n                        }\n                    };\n                }\n                if (typeof value === \"object\" && value !== null) {\n                    return createDeepProxy(value, cache);\n                }\n                return value;\n            },\n        });\n        cache.set(target, proxy);\n        return proxy;\n    };\n\n    return createDeepProxy(sdk);\n};\n"
  },
  {
    "path": "electron/mapi/manager/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\nimport {\n    ActionRecord,\n    ConfigRecord,\n    PluginRecord,\n} from \"../../../src/types/Manager\";\n\nconst getConfig = async () => {\n    return ipcRenderer.invoke(\"manager:getConfig\");\n};\n\nconst setConfig = async (config: ConfigRecord) => {\n    return ipcRenderer.invoke(\"manager:setConfig\", config);\n};\n\nconst getMcpServer = async () => {\n    return ipcRenderer.invoke(\"manager:getMcpServer\");\n};\n\nconst getMcpInfo = async () => {\n    return ipcRenderer.invoke(\"manager:getMcpInfo\");\n};\n\nconst isShown = async () => {\n    return ipcRenderer.invoke(\"manager:isShown\");\n};\n\nconst show = async () => {\n    return ipcRenderer.invoke(\"manager:show\");\n};\n\nconst hide = async () => {\n    return ipcRenderer.invoke(\"manager:hide\");\n};\n\nconst getClipboardContent = () => {\n    return ipcRenderer.invoke(\"manager:getClipboardContent\");\n};\n\nconst getClipboardChangeTime = () => {\n    return ipcRenderer.invoke(\"manager:getClipboardChangeTime\");\n};\n\nconst getSelectedContent = async () => {\n    return ipcRenderer.invoke(\"manager:getSelectedContent\");\n};\n\nconst listPlugin = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:listPlugin\", option);\n};\n\nconst installPlugin = async (fileOrPath: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:installPlugin\", fileOrPath, option);\n};\n\nconst refreshInstallPlugin = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\n        \"manager:refreshInstallPlugin\",\n        pluginName,\n        option,\n    );\n};\n\nconst uninstallPlugin = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:uninstallPlugin\", pluginName, option);\n};\n\nconst getPluginInstalledVersion = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\n        \"manager:getPluginInstalledVersion\",\n        pluginName,\n        option,\n    );\n};\n\nconst listDisabledActionMatch = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:listDisabledActionMatch\", option);\n};\n\nconst toggleDisabledActionMatch = async (\n    pluginName: string,\n    actionName: string,\n    matchName: string,\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\n        \"manager:toggleDisabledActionMatch\",\n        pluginName,\n        actionName,\n        matchName,\n        option,\n    );\n};\n\nconst listPinAction = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:listPinAction\", option);\n};\n\nconst togglePinAction = async (\n    pluginName: string,\n    actionName: string,\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\n        \"manager:togglePinAction\",\n        pluginName,\n        actionName,\n        option,\n    );\n};\n\nconst showLog = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:showLog\", pluginName, option);\n};\n\nconst clearCache = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:clearCache\", option);\n};\n\nconst hotKeyWatch = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:hotKeyWatch\", option);\n};\n\nconst hotKeyUnwatch = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:hotKeyUnwatch\", option);\n};\n\nconst searchFastPanelAction = async (\n    query: {\n        currentFiles: any[];\n        currentImage: string;\n    },\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\"manager:searchFastPanelAction\", query, option);\n};\n\nconst searchAction = async (\n    query: {\n        keywords: string;\n        currentFiles: any[];\n        currentImage: string;\n    },\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\"manager:searchAction\", query, option);\n};\n\nconst listDetachWindowActions = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:listDetachWindowActions\", option);\n};\n\nconst subInputChange = (keywords: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:subInputChange\", keywords, option);\n};\n\nconst openPlugin = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:openPlugin\", pluginName, option);\n};\n\nconst openAction = async (action: ActionRecord) => {\n    return ipcRenderer.invoke(\"manager:openAction\", action);\n};\n\nconst openActionCode = async (id: string) => {\n    return ipcRenderer.invoke(\"manager:openActionCode\", id);\n};\n\nconst searchActionCode = async (keywords: string) => {\n    return ipcRenderer.invoke(\"manager:searchActionCode\", keywords);\n};\n\nconst openActionWindow = async (\n    type: \"open\" | \"close\",\n    action: ActionRecord,\n) => {\n    return ipcRenderer.invoke(\"manager:openActionWindow\", type, action);\n};\n\nconst closeMainPlugin = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:closeMainPlugin\", option);\n};\n\nconst openMainPluginDevTools = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:openMainPluginDevTools\", option);\n};\n\nconst openMainPluginLog = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:openMainPluginLog\", option);\n};\n\nconst detachPlugin = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:detachPlugin\", option);\n};\n\nconst toggleDetachPluginAlwaysOnTop = async (\n    alwaysOnTop: boolean,\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\n        \"manager:toggleDetachPluginAlwaysOnTop\",\n        alwaysOnTop,\n        option,\n    );\n};\n\nconst setDetachPluginZoom = async (zoom: number, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:setDetachPluginZoom\", zoom, option);\n};\n\nconst firePluginMoreMenuClick = async (name: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:firePluginMoreMenuClick\", name, option);\n};\n\nconst fireDetachOperateClick = async (name: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:fireDetachOperateClick\", name, option);\n};\n\nconst closeDetachPlugin = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:closeDetachPlugin\");\n};\n\nconst openDetachPluginDevTools = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:openDetachPluginDevTools\", option);\n};\n\nconst openDetachPluginLog = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:openDetachPluginLog\", option);\n};\n\nconst setPluginAutoDetach = async (autoDetach: boolean, option?: {}) => {\n    return ipcRenderer.invoke(\n        \"manager:setPluginAutoDetach\",\n        autoDetach,\n        option,\n    );\n};\n\nconst getPluginConfig = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:getPluginConfig\", pluginName, option);\n};\n\nconst listFilePluginRecords = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:listFilePluginRecords\", option);\n};\n\nconst updateFilePluginRecords = async (\n    records: PluginRecord[],\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\n        \"manager:updateFilePluginRecords\",\n        records,\n        option,\n    );\n};\n\nconst listLaunchRecords = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:listLaunchRecords\", option);\n};\n\nconst updateLaunchRecords = async (records: PluginRecord[], option?: {}) => {\n    return ipcRenderer.invoke(\"manager:updateLaunchRecords\", records, option);\n};\n\nconst storeInstall = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:storeInstall\", pluginName, option);\n};\n\nconst storePublish = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:storePublish\", pluginName, option);\n};\n\nconst storePublishInfo = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:storePublishInfo\", pluginName, option);\n};\n\nconst storeInstallingInfo = async (pluginName: string, option?: {}) => {\n    return ipcRenderer.invoke(\n        \"manager:storeInstallingInfo\",\n        pluginName,\n        option,\n    );\n};\n\nconst clipboardList = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:clipboardList\", option);\n};\n\nconst clipboardClear = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:clipboardClear\", option);\n};\n\nconst clipboardDelete = async (timestamp: number, option?: {}) => {\n    return ipcRenderer.invoke(\"manager:clipboardDelete\", timestamp, option);\n};\n\nconst historyClear = async (option?: {}) => {\n    return ipcRenderer.invoke(\"manager:historyClear\", option);\n};\n\nconst historyDelete = async (\n    pluginName: string,\n    actionName: string,\n    option?: {},\n) => {\n    return ipcRenderer.invoke(\n        \"manager:historyDelete\",\n        pluginName,\n        actionName,\n        option,\n    );\n};\n\nexport default {\n    getConfig,\n    setConfig,\n\n    getMcpServer,\n    getMcpInfo,\n\n    isShown,\n    show,\n    hide,\n\n    getClipboardContent,\n    getClipboardChangeTime,\n    getSelectedContent,\n    listPlugin,\n    installPlugin,\n    refreshInstallPlugin,\n    uninstallPlugin,\n    getPluginInstalledVersion,\n    listDisabledActionMatch,\n    toggleDisabledActionMatch,\n    listPinAction,\n    togglePinAction,\n    showLog,\n    clearCache,\n    hotKeyWatch,\n    hotKeyUnwatch,\n\n    searchFastPanelAction,\n    searchAction,\n    listDetachWindowActions,\n    subInputChange,\n    openPlugin,\n    openAction,\n    openActionCode,\n    searchActionCode,\n    openActionWindow,\n    closeMainPlugin,\n    openMainPluginDevTools,\n    openMainPluginLog,\n    detachPlugin,\n\n    toggleDetachPluginAlwaysOnTop,\n    setDetachPluginZoom,\n    firePluginMoreMenuClick,\n    fireDetachOperateClick,\n    closeDetachPlugin,\n    openDetachPluginDevTools,\n    openDetachPluginLog,\n    setPluginAutoDetach,\n    getPluginConfig,\n\n    listFilePluginRecords,\n    updateFilePluginRecords,\n    listLaunchRecords,\n    updateLaunchRecords,\n\n    storeInstall,\n    storePublish,\n    storePublishInfo,\n    storeInstallingInfo,\n\n    clipboardList,\n    clipboardClear,\n    clipboardDelete,\n\n    historyClear,\n    historyDelete,\n};\n"
  },
  {
    "path": "electron/mapi/manager/storage/index.ts",
    "content": "export const ManagerStorage = {\n    listPlugins() {},\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/asset/icon.ts",
    "content": "import pluginSystem from \"./plugin-system.svg\";\nimport pluginStore from \"./plugin-store.svg\";\nimport pluginWorkflow from \"./plugin-workflow.svg\";\nimport pluginApp from \"./plugin-app.svg\";\n\nimport searchKeyword from \"./search-keyword.svg\";\nimport searchMatch from \"./search-match.svg\";\nimport searchWindow from \"./search-window.svg\";\n\nimport command from \"./command.svg\";\nimport database from \"./database.svg\";\nimport folder from \"./folder.svg\";\nimport model from \"./model.svg\";\nimport mcp from \"./mcp.svg\";\nimport screenshot from \"./screenshot.svg\";\nimport colorPicker from \"./color-picker.svg\";\nimport screenRecord from \"./screen-record.svg\";\nimport plugin from \"./plugin.svg\";\nimport thunder from \"./thunder.svg\";\nimport guide from \"./guide.svg\";\nimport user from \"./user.svg\";\nimport about from \"./about.svg\";\nimport apple from \"./apple.svg\";\nimport windows from \"./windows.svg\";\nimport linux from \"./linux.svg\";\nimport lock from \"./lock.svg\";\nimport ip from \"./ip.svg\";\n\nexport const SystemIcons = {\n    pluginSystem,\n    pluginStore,\n    pluginWorkflow,\n    pluginApp,\n\n    searchKeyword,\n    searchMatch,\n    searchWindow,\n\n    command,\n    database,\n    folder,\n    model,\n    mcp,\n    screenshot,\n    colorPicker,\n    screenRecord,\n    plugin,\n    thunder,\n    guide,\n    user,\n    about,\n    apple,\n    windows,\n    linux,\n    lock,\n    ip,\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/index.ts",
    "content": "import {\n    ActionRecord,\n    PluginRecord,\n    PluginType,\n} from \"../../../../src/types/Manager\";\nimport { SystemPlugin } from \"./plugin/system\";\nimport { SystemActionCode } from \"./plugin/system/action\";\nimport { StorePlugin } from \"./plugin/store\";\nimport { StoreActionCode } from \"./plugin/store/action\";\nimport { MemoryCacheUtil } from \"../../../lib/util\";\nimport { ManagerPlugin } from \"../plugin\";\nimport { getAppPlugin } from \"./plugin/app\";\nimport { getFilePlugin } from \"./plugin/file\";\n\nconst pluginActionCode = {\n    system: SystemActionCode,\n    store: StoreActionCode,\n};\n\nconst systemPlugin = new Set([\"system\", \"store\", \"workflow\", \"app\", \"file\"]);\n\nconst pluginActionBackend = {};\n\nexport const ManagerSystem = {\n    async clearCache() {\n        for (const p of await this.list()) {\n            delete p.runtime;\n        }\n        MemoryCacheUtil.forget(\"SystemActions\");\n    },\n    match(name: string) {\n        return systemPlugin.has(name);\n    },\n    async list() {\n        const plugins: (PluginRecord | any)[] = [\n            SystemPlugin,\n            StorePlugin,\n            getAppPlugin,\n            getFilePlugin,\n        ];\n        for (let i = 0; i < plugins.length; i++) {\n            if (typeof plugins[i] === \"function\") {\n                plugins[i] = await plugins[i]();\n            }\n            plugins[i] = await ManagerPlugin.initIfNeed(plugins[i], {\n                type: PluginType.SYSTEM,\n                root: null,\n            });\n        }\n        return plugins as PluginRecord[];\n    },\n    getActionCodeFunc(pluginName: string, name: string) {\n        if (!pluginActionCode[pluginName]) {\n            return null;\n        }\n        return pluginActionCode[pluginName][name] || null;\n    },\n    getActionBackendFunc(pluginName: string, name: string) {\n        if (!pluginActionBackend[pluginName]) {\n            return null;\n        }\n        return pluginActionBackend[pluginName][name] || null;\n    },\n    async listAction() {\n        return await MemoryCacheUtil.remember<ActionRecord[]>(\n            \"SystemActions\",\n            async () => {\n                let actions: ActionRecord[] = [];\n                const plugins = await this.list();\n                for (const p of plugins) {\n                    actions = actions.concat(p.actions);\n                }\n                return actions;\n            },\n        );\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/linux/icon.ts",
    "content": "import path from \"node:path\";\nimport fs from \"node:fs\";\n\nexport const getIcon = async (\n    desktopInfo: Record<string, string>,\n    pathname: string,\n    name: string,\n) => {\n    if (!desktopInfo.Icon) {\n        return null;\n    }\n    const themes = [\"hicolor\"];\n    const sizes = [\"scalable\", \"512x512\", \"256x256\", \"48x48\", \"32x32\"];\n    const types = [\"apps\"];\n    const exts = [\".png\", \".svg\"];\n    for (const theme of themes) {\n        for (const size of sizes) {\n            for (const type of types) {\n                for (const ext of exts) {\n                    let iconPath = path.join(\n                        \"/usr/share/icons\",\n                        theme,\n                        size,\n                        type,\n                        desktopInfo.Icon + ext,\n                    );\n                    if (fs.existsSync(iconPath)) {\n                        return \"file://\" + iconPath;\n                    }\n                }\n            }\n        }\n    }\n    return null;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/linux/index.ts",
    "content": "import { listFiles } from \"../util\";\nimport path from \"path\";\nimport { ConfigLang } from \"../../../../../../config/lang\";\nimport { getIcon } from \"./icon\";\nimport { getAppTitle } from \"./title\";\nimport fs from \"node:fs\";\n\nexport const ManagerAppLinux = {\n    list: async () => {\n        return lists();\n    },\n};\n\nconst appSet = new Set<string>();\n\nconst lists = async () => {\n    appSet.clear();\n    const files = await listFiles([\n        \"/usr/share/applications\",\n        \"/var/lib/snapd/desktop/applications\",\n        `${process.env.HOME}/.local/share/applications`,\n    ]);\n    const apps = [];\n    const locale = ConfigLang.getLocale();\n    for (const f of files) {\n        if (appSet.has(f.pathname)) {\n            // console.log('appSet.has', f.pathname)\n            continue;\n        }\n        const extname = path.extname(f.pathname);\n        if (extname !== \".desktop\") {\n            continue;\n        }\n        const app = {\n            name: f.name.replace(/\\.(desktop)$/, \"\"),\n            title: f.name,\n            pathname: f.pathname,\n            icon: null,\n            command: null,\n        };\n        const desktopInfo = await parseDesktopFile(app.pathname);\n        app.icon = await getIcon(desktopInfo, app.pathname, app.name);\n        app.title = await getAppTitle(\n            desktopInfo,\n            locale,\n            app.pathname,\n            app.name,\n        );\n        if (!app.icon) {\n            continue;\n        }\n        if (!desktopInfo.Exec) {\n            continue;\n        }\n        let command = desktopInfo.Exec.replace(/ %[A-Za-z]/g, \"\")\n            .replace(/\"/g, \"\")\n            .trim();\n        if (desktopInfo.Terminal === \"true\") {\n            command = `gnome-terminal -x ${command}`;\n        }\n        app.command = command;\n        appSet.add(app.pathname);\n        apps.push(app);\n    }\n    return apps;\n};\n\nconst parseDesktopFile = async (\n    pathname: string,\n): Promise<Record<string, string>> => {\n    const content = fs.readFileSync(pathname, \"utf-8\");\n    const desktop = {};\n    for (const line of content.split(\"\\n\")) {\n        if (line.startsWith(\"[\")) {\n            continue;\n        }\n        const [key, value] = line.split(\"=\");\n        if (!key || !value) {\n            continue;\n        }\n        desktop[key] = value;\n    }\n    return desktop;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/linux/title.ts",
    "content": "const langDirMap = {\n    \"zh-CN\": [\"zh_CN\"],\n};\n\nexport const getAppTitle = async (\n    desktopInfo: Record<string, string>,\n    locale: string,\n    pathname: string,\n    name: string,\n) => {\n    if (locale in langDirMap) {\n        for (const k of langDirMap[locale]) {\n            const infoKey = `Name[${k}]`;\n            if (desktopInfo[infoKey]) {\n                return desktopInfo[infoKey];\n            }\n        }\n    }\n    if (desktopInfo.Name) {\n        return desktopInfo.Name;\n    }\n    return name;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/mac/icon.ts",
    "content": "import path from \"node:path\";\nimport fs from \"fs\";\nimport { exec } from \"child_process\";\nimport { Files } from \"../../../../../file/main\";\nimport { AppEnv, waitAppEnvReady } from \"../../../../../env\";\n\nconst getIconTempDir = async () => {\n    await waitAppEnvReady();\n    return path.join(AppEnv.dataRoot, \"cache\", \"app-icons\");\n};\n// console.log('iconTempDir', iconTempDir)\nconst defaultIcon =\n    \"/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns\";\n\nconst getIconFile = (appFileInput) => {\n    return new Promise((resolve, reject) => {\n        const plistPath = path.join(appFileInput, \"Contents\", \"Info.plist\");\n        Files.read(plistPath, {\n            isDataPath: false,\n        })\n            .then((plistContent) => {\n                if (plistContent) {\n                    // parse CFBundleIconFile\n                    const mat = plistContent.match(\n                        /<key>CFBundleIconFile<\\/key>\\s*<string>(.*?)<\\/string>/,\n                    );\n                    if (mat) {\n                        const CFBundleIconFile = mat[1];\n                        const iconFile = path.join(\n                            appFileInput,\n                            \"Contents\",\n                            \"Resources\",\n                            CFBundleIconFile,\n                        );\n                        const iconFiles = [\n                            iconFile,\n                            iconFile + \".icns\",\n                            iconFile + \".tiff\",\n                        ];\n                        const existedIcon = iconFiles.find((iconFile) => {\n                            return fs.existsSync(iconFile);\n                        });\n                        // console.log('manager.app.mac.app2png.getIconFile', existedIcon)\n                        resolve(existedIcon || defaultIcon);\n                        return;\n                    }\n                }\n                resolve(defaultIcon);\n            })\n            .catch((e) => {\n                console.log(\"manager.app.mac.app2png.getIconFile.error\", e);\n                resolve(defaultIcon);\n            });\n    });\n};\n\nconst tiffToPng = (iconFile, pngFileOutput) => {\n    return new Promise((resolve, reject) => {\n        exec(\n            `sips -s format png '${iconFile}' --out '${pngFileOutput}' --resampleHeightWidth 64 64`,\n            (error) => {\n                error ? reject(error) : resolve(null);\n            },\n        );\n    });\n};\n\nconst app2png = (appFileInput, pngFileOutput) => {\n    return getIconFile(appFileInput).then((iconFile) => {\n        // console.log('manager.app.mac.app2png.app2png', iconFile, pngFileOutput)\n        return tiffToPng(iconFile, pngFileOutput);\n    });\n};\n\nexport const getIcon = async (appPath: string, appName: string) => {\n    try {\n        const iconTempDir = await getIconTempDir();\n        const iconPathUrl =\n            \"file://\" +\n            path.join(iconTempDir, `${encodeURIComponent(appName)}.png`);\n        const iconPath = path.join(iconTempDir, `${appName}.png`);\n        if (await Files.exists(iconPath, { isDataPath: false })) {\n            return iconPathUrl;\n        }\n        const iconNone = path.join(iconTempDir, `${appName}.none`);\n        const iconNoneUrl = path.join(iconTempDir, `${appName}.none`);\n        if (await Files.exists(iconNone, { isDataPath: false })) {\n            return iconNoneUrl;\n        }\n        if (!(await Files.exists(iconTempDir, { isDataPath: false }))) {\n            fs.mkdirSync(iconTempDir, { recursive: true });\n        }\n        await app2png(appPath, iconPath);\n        if (!(await Files.exists(iconPath, { isDataPath: false }))) {\n            fs.writeFileSync(iconNone, \"\");\n            throw \"IconGetError\";\n        }\n        return iconPathUrl;\n    } catch (e) {}\n    return `file://${defaultIcon}`;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/mac/index.ts",
    "content": "import { listFiles } from \"../util\";\nimport path from \"node:path\";\nimport { AppRecord } from \"../type\";\nimport { getIcon } from \"./icon\";\nimport { ConfigLang } from \"../../../../../../config/lang\";\nimport { getAppTitle } from \"./title\";\n\nconst appSet = new Set<string>();\n\nexport const ManagerAppMac = {\n    list: async () => {\n        return lists();\n    },\n};\n\nconst lists = async (): Promise<AppRecord[]> => {\n    appSet.clear();\n    let files = await listFiles([\n        \"/Applications\",\n        \"~/Applications\",\n        \"/System/Applications\",\n        \"/System/Library/PreferencePanes\",\n    ]);\n    const apps = [];\n    const locale = ConfigLang.getLocale();\n    for (const f of files) {\n        if (appSet.has(f.pathname)) {\n            // console.log('appSet.has', f.pathname)\n            continue;\n        }\n        const extname = path.extname(f.pathname);\n        if (extname !== \".app\" && extname !== \".prefPane\") {\n            continue;\n        }\n        const app = {\n            name: f.name.replace(/\\.(app|prefPane)$/, \"\"),\n            title: f.name,\n            pathname: f.pathname,\n            icon: null,\n            command: null,\n        };\n        app.icon = await getIcon(app.pathname, app.name);\n        app.title = await getAppTitle(locale, app.pathname, app.name);\n        if (!app.icon) {\n            continue;\n        }\n        app.command = `open ${app.pathname.replace(/ /g, \"\\\\ \") as string}`;\n        appSet.add(app.pathname);\n        apps.push(app);\n    }\n    return apps;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/mac/title.ts",
    "content": "import { Files } from \"../../../../../file/main\";\nimport { IconvUtil } from \"../../../../../../lib/util\";\n\nconst langDirMap = {\n    \"en-US\": [\"en.lproj\"],\n    \"zh-CN\": [\"zh-Hans.lproj\", \"zh_CN.lproj\"],\n};\n\nexport const getAppTitle = async (\n    locale: string,\n    pathname: string,\n    name: string,\n) => {\n    if (!(locale in langDirMap)) {\n        return name;\n    }\n    const langDirs = langDirMap[locale];\n    // console.log('langDirs', langDirs)\n    for (const langDir of langDirs) {\n        const infoPlistPath =\n            pathname + \"/Contents/Resources/\" + langDir + \"/InfoPlist.strings\";\n        // console.log('infoPlistPath', infoPlistPath)\n        if (!(await Files.exists(infoPlistPath, { isDataPath: false }))) {\n            continue;\n        }\n        const buffer = await Files.readBuffer(infoPlistPath, {\n            isDataPath: false,\n        });\n        const content = IconvUtil.bufferToUtf8(buffer) as string;\n        // console.log('content', infoPlistPath, content.toString('utf8'))\n        // CFBundleName = \"网易邮箱大师\";\n        if (content) {\n            // console.log('content', JSON.stringify(content))\n            // CFBundleDisplayName = \"网易邮箱大师\";\n            const reg = new RegExp('\"?CFBundleDisplayName\"?.*?=.*?\"(.*)\".*?;');\n            const match = content.match(reg);\n            if (match) {\n                // console.log('content.result', match[1])\n                return match[1];\n            }\n        }\n    }\n    // console.log('===============')\n    // console.log('getAppTitle', locale, pathname, name)\n    return name;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/type.ts",
    "content": "export type AppRecord = {\n    name: string;\n    title: string;\n    pathname: string;\n    icon: string;\n    command: string;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/util/index.ts",
    "content": "import { Files } from \"../../../../../file/main\";\n\nexport const listFiles = async (\n    paths: string[],\n): Promise<\n    {\n        name: string;\n        pathname: string;\n        isDirectory: boolean;\n        size: number;\n        lastModified: number;\n    }[]\n> => {\n    let results: any[] = [];\n    for (const path of paths) {\n        for (let p of await Files.list(path, {\n            isDataPath: false,\n        })) {\n            results.push(p);\n        }\n    }\n    return results;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/win/icon.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\n\nimport extractFileIcon from \"extract-file-icon\";\nimport { AppEnv, waitAppEnvReady } from \"../../../../../env\";\n\nconst getIconTempDir = async () => {\n    await waitAppEnvReady();\n    return path.join(AppEnv.dataRoot, \"cache\", \"app-icons\");\n};\n\nexport const getIcon = async (appPath: string, appName: string) => {\n    try {\n        const iconTempDir = await getIconTempDir();\n        const iconPath = path.join(iconTempDir, `${appName}.png`);\n        const iconPathUrl = `file://${iconPath}`;\n        // console.log('iconPath', iconPath, appName, appPath);\n        if (fs.existsSync(iconPath)) {\n            return iconPathUrl;\n        }\n        const iconNone = path.join(iconTempDir, `${appName}.none`);\n        const iconNoneUrl = `file://${iconNone}`;\n        if (fs.existsSync(iconNone)) {\n            return iconNoneUrl;\n        }\n        if (!fs.existsSync(iconTempDir)) {\n            fs.mkdirSync(iconTempDir, { recursive: true });\n        }\n        const buffer = extractFileIcon(appPath, 32);\n        fs.writeFileSync(iconPath, buffer, \"base64\");\n        if (fs.existsSync(iconPath)) {\n            return iconPathUrl;\n        } else {\n            fs.writeFileSync(iconNone, \"\");\n        }\n    } catch (e) {}\n    return null;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/win/index.ts",
    "content": "import { listFiles } from \"../util\";\nimport path from \"path\";\nimport os from \"os\";\nimport { shell } from \"electron\";\nimport { AppRecord } from \"../type\";\nimport { ShellUtil } from \"../../../../../../lib/util\";\nimport { getIcon } from \"./icon\";\nimport { getAppTitle } from \"./title\";\n\nexport const ManagerAppWin = {\n    list: async () => {\n        return lists();\n    },\n};\n\nconst apps: AppRecord[] = [];\nconst appSet = new Set<string>();\n\nconst blackList = [\"msiexec.exe\"];\n\nconst readDir = async (dir: string) => {\n    let files = await listFiles([dir]);\n    for (const f of files) {\n        if (f.isDirectory) {\n            await readDir(f.pathname);\n        } else {\n            let name = f.name.split(\".\")[0];\n            let appDetail: any = {};\n            try {\n                appDetail = shell.readShortcutLink(f.pathname);\n            } catch (e) {\n                //\n            }\n            const pathname = appDetail.target;\n            if (\n                !pathname ||\n                appSet.has(pathname) ||\n                !pathname.endsWith(\".exe\") ||\n                pathname.endsWith(\"uninst.exe\") ||\n                pathname.endsWith(\"uninstall.exe\")\n            ) {\n                continue;\n            }\n            appSet.add(pathname);\n            name = path.basename(appDetail.target, \".exe\");\n            if (blackList.includes(name)) {\n                continue;\n            }\n            const title = await getAppTitle(\"zh-CN\", pathname, name);\n            const app = {\n                name,\n                title,\n                pathname,\n                icon: await getIcon(appDetail.target, name),\n                command: `start \"dummyclient\" ${ShellUtil.quotaPath(appDetail.target)}`,\n            };\n            // console.log('app', app)\n            apps.push(app);\n        }\n    }\n};\n\nconst lists = async (): Promise<AppRecord[]> => {\n    appSet.clear();\n    await readDir(\"C:\\\\ProgramData\\\\Microsoft\\\\Windows\\\\Start Menu\\\\Programs\");\n    await readDir(\n        path.join(\n            os.homedir(),\n            \"./AppData/Roaming\",\n            \"Microsoft\\\\Windows\\\\Start Menu\\\\Programs\",\n        ),\n    );\n    // console.log('apps', apps)\n    return apps;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app/win/title.ts",
    "content": "import { exec } from \"child_process\";\nimport { IconvUtil } from \"../../../../../../lib/util\";\n\nexport const getAppTitle = async (\n    locale: string,\n    pathname: string,\n    name: string,\n) => {\n    // (Get-ItemProperty -Path 'C:\\\\Program Files (x86)\\\\360\\\\360zip\\\\360zip.exe').VersionInfo.FileDescription\n    // (Get-ItemProperty -Path 'C:\\\\Program Files (x86)\\\\360\\\\360Safe\\\\360Safe.exe').VersionInfo.FileDescription\n    // (Get-ItemProperty -Path 'C:\\\\Windows\\\\SysWOW64\\\\msiexec.exe').VersionInfo.FileDescription\n    const command = `powershell -Command \"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; (Get-ItemProperty -Path '${pathname}').VersionInfo.FileDescription\"`;\n    return new Promise<string>((resolve, reject) => {\n        exec(\n            command,\n            {\n                encoding: \"utf-8\",\n            },\n            (error, stdout, stderr) => {\n                if (error) {\n                    resolve(name);\n                } else {\n                    // console.log('win.getAppTitle', {\n                    //     locale,\n                    //     pathname,\n                    //     name,\n                    //     stdout: stdout,\n                    //     title: stdout.toString()?.trim(),\n                    // })\n                    resolve(stdout.toString()?.trim());\n                }\n            },\n        );\n    });\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/app.ts",
    "content": "import {\n    ActionMatch,\n    ActionMatchTypeEnum,\n    ActionRecord,\n    ActionTypeEnum,\n    PluginRecord,\n} from \"../../../../../src/types/Manager\";\nimport { t } from \"../../../../config/lang\";\nimport { isLinux, isMac, isWin } from \"../../../../lib/env\";\nimport { MemoryCacheUtil } from \"../../../../lib/util\";\nimport { ManagerFileCacheUtil } from \"../../lib/cache\";\nimport { Manager } from \"../../manager\";\nimport { SystemIcons } from \"../asset/icon\";\nimport { ManagerSystem } from \"../index\";\nimport { ManagerAppLinux } from \"./app/linux\";\nimport { ManagerAppMac } from \"./app/mac\";\nimport { AppRecord } from \"./app/type\";\nimport { ManagerAppWin } from \"./app/win\";\n\nlet logo = SystemIcons.windows;\nif (isMac) {\n    logo = SystemIcons.apple;\n} else if (isLinux) {\n    logo = SystemIcons.linux;\n}\n\nexport const AppPlugin: PluginRecord = {\n    name: \"app\",\n    title: t(\"system.apps\"),\n    version: \"1.0.0\",\n    logo: logo,\n    description: t(\"system.appsDesc\"),\n    main: null,\n    preload: null,\n    actions: [],\n};\n\nconst list = async () => {\n    let apps: AppRecord[] = [];\n    if (isMac) {\n        apps = await ManagerAppMac.list();\n    } else if (isWin) {\n        apps = await ManagerAppWin.list();\n    } else if (isLinux) {\n        apps = await ManagerAppLinux.list();\n    }\n    return apps;\n};\n\nconst listActions = async () => {\n    // await sleep(3500)\n    return await MemoryCacheUtil.remember(\"AppActions\", async () => {\n        const actions: ActionRecord[] = [];\n        const apps = await list();\n        apps.forEach((app) => {\n            const matches: ActionMatch[] = [];\n            matches.push({\n                type: ActionMatchTypeEnum.TEXT,\n                text: app.name,\n            } as ActionMatch);\n            if (app.title !== app.name) {\n                matches.push({\n                    type: ActionMatchTypeEnum.TEXT,\n                    text: app.title,\n                } as ActionMatch);\n            }\n            actions.push({\n                fullName: `${AppPlugin.name}/${app.name}`,\n                pluginName: AppPlugin.name,\n                name: app.name,\n                title: app.title,\n                icon: app.icon,\n                type: ActionTypeEnum.COMMAND,\n                matches: matches,\n                data: {\n                    command: app.command,\n                },\n            });\n        });\n        // console.log('actions', actions)\n        return actions;\n    });\n};\n\ntype ActionInfo = {\n    time: number;\n    actions: ActionRecord[];\n};\n\nlet listActionRunning = false;\nlet listActionFirstRunning = true;\nexport const getAppPlugin = async () => {\n    AppPlugin.actions = [];\n    let toastTimer = null;\n    const cacheInfo = await ManagerFileCacheUtil.getIgnoreExpire(\n        \"AppActions\",\n        [],\n    );\n    AppPlugin.actions = cacheInfo.value;\n    let shouldNotice = false;\n    if (!cacheInfo.isCache || cacheInfo.expire < Date.now()) {\n        if (!listActionRunning) {\n            listActionRunning = true;\n            if (listActionFirstRunning) {\n                listActionFirstRunning = false;\n                shouldNotice = true;\n            }\n            listActions().then((actions) => {\n                // console.log('find.actions', actions)\n                AppPlugin.actions = actions;\n                ManagerFileCacheUtil.set(\"AppActions\", actions, 1000 * 3600);\n                if (toastTimer) {\n                    clearTimeout(toastTimer);\n                } else if (shouldNotice) {\n                    Manager.setNotice({\n                        text: t(\"system.appsIndexed\"),\n                        type: \"success\",\n                        duration: 5000,\n                    }).then();\n                }\n                listActionRunning = false;\n                ManagerSystem.clearCache();\n            });\n        }\n    }\n    if (!AppPlugin.actions.length && shouldNotice) {\n        toastTimer = setTimeout(() => {\n            Manager.setNotice(t(\"system.appsIndexing\")).then();\n            toastTimer = null;\n        }, 3000);\n    }\n    return AppPlugin;\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/file.ts",
    "content": "import {\n    ActionMatch,\n    ActionMatchTypeEnum,\n    ActionRecord,\n    ActionTypeEnum,\n    FilePluginRecord,\n    PluginRecord,\n} from \"../../../../../src/types/Manager\";\nimport { CommonConfig } from \"../../../../config/common\";\nimport { t } from \"../../../../config/lang\";\nimport { MemoryCacheUtil, ShellUtil } from \"../../../../lib/util\";\nimport { KVDBMain } from \"../../../kvdb/main\";\nimport { SystemIcons } from \"../asset/icon\";\nimport { ManagerSystem } from \"../index\";\n\nexport const FilePlugin: PluginRecord = {\n    name: \"file\",\n    title: t(\"system.fileLaunch\"),\n    version: \"1.0.0\",\n    logo: SystemIcons.folder,\n    description: t(\"system.fileLaunchDesc\"),\n    main: null,\n    preload: null,\n    actions: [],\n};\n\nconst listActions = async () => {\n    return await MemoryCacheUtil.remember<ActionRecord[]>(\n        \"FileActions\",\n        async () => {\n            const actions: ActionRecord[] = [];\n            const records = await ManagerSystemPluginFile.list();\n            records.forEach((record, recordIndex) => {\n                actions.push({\n                    fullName: `${FilePlugin.name}/${record.title}`,\n                    pluginName: FilePlugin.name,\n                    name: record.title,\n                    title: record.title,\n                    icon: record.icon,\n                    type: ActionTypeEnum.COMMAND,\n                    matches: [\n                        {\n                            type: ActionMatchTypeEnum.TEXT,\n                            text: record.title,\n                        } as ActionMatch,\n                    ],\n                    data: {\n                        command: \"open \" + ShellUtil.quotaPath(record.path),\n                    },\n                });\n            });\n            return actions;\n        },\n    );\n};\n\nexport const getFilePlugin = async () => {\n    FilePlugin.actions = await listActions();\n    return FilePlugin;\n};\n\nexport const ManagerSystemPluginFile = {\n    async list(): Promise<FilePluginRecord[]> {\n        return MemoryCacheUtil.remember(\"Files\", async () => {\n            const res = await KVDBMain.getData(\n                CommonConfig.dbSystem,\n                CommonConfig.dbFileId,\n            );\n            if (res) {\n                return res[\"records\"] || [];\n            }\n            return [];\n        });\n    },\n    async update(records: FilePluginRecord[]) {\n        await KVDBMain.putForce(CommonConfig.dbSystem, {\n            _id: CommonConfig.dbFileId,\n            records: records,\n        });\n        MemoryCacheUtil.forget(\"Files\");\n        MemoryCacheUtil.forget(\"FileActions\");\n        await ManagerSystem.clearCache();\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/store/action.ts",
    "content": "import { ActionTypeCodeData } from \"../../../../../../src/types/Manager\";\nimport { screenCapture } from \"../../../plugin/screenCapture\";\nimport { AppsMain } from \"../../../../app/main\";\n\nexport const StoreActionCode = {};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/store/index.ts",
    "content": "import { PluginType } from \"../../../../../../src/types/Manager\";\nimport { Files } from \"../../../../file/main\";\nimport { UserApi } from \"../../../../user/main\";\nimport { Manager } from \"../../../manager\";\nimport { ManagerPlugin } from \"../../../plugin\";\n// @ts-ignore\nimport fs from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { mapError } from \"../../../../../../src/lib/error\";\nimport { t } from \"../../../../../config/lang\";\nimport { MarkdownUtil } from \"../../../../../lib/util\";\nimport { AppsMain } from \"../../../../app/main\";\nimport { Misc } from \"../../../../misc\";\n\nexport const ManagerPluginStore = {\n    installingMap: {} as {\n        [pluginName: string]: {\n            name: string;\n            percent: number;\n            startTime: number;\n        };\n    },\n    async install(\n        pluginName: string,\n        option?: {\n            version?: string;\n        },\n    ) {\n        this.installingMap[pluginName] = {\n            name: pluginName,\n            percent: 0,\n            startTime: Date.now(),\n        };\n        option = Object.assign(\n            {\n                version: null,\n            },\n            option,\n        );\n        const payload = {\n            plugin: pluginName,\n            version: option[\"version\"],\n        };\n        const existPlugin = await ManagerPlugin.get(pluginName);\n        let isUpgrade = false;\n        if (existPlugin && existPlugin.version !== option[\"version\"]) {\n            isUpgrade = true;\n        }\n        try {\n            if (isUpgrade) {\n                await ManagerPlugin.uninstall(pluginName);\n            }\n            const infoRes = await UserApi.post(\n                \"store/plugin_info_guest\",\n                payload,\n            );\n            await ManagerPlugin.configCheck(infoRes.data[\"config\"]);\n            // console.log('ManagerPluginStore.install', JSON.stringify({pluginName, option, data: infoRes.data}, null, 2));\n            const packageRes = await UserApi.post(\n                \"store/plugin_package_guest\",\n                payload,\n            );\n            const packageUrl = packageRes.data[\"package\"];\n            const packageMd5 = packageRes.data[\"packageMd5\"];\n            // console.log('ManagerPluginStore.install', JSON.stringify({pluginName, option, packageRes}, null, 2));\n            const tempFile = await Files.temp(\"zip\");\n            // console.log('ManagerPluginStore.install', JSON.stringify({pluginName, option, tempFile}, null, 2));\n            // console.log('ManagerPluginStore.install.downloadStart');\n            let lastPercent = 0;\n            await Files.download(packageUrl, tempFile, {\n                isDataPath: false,\n                progress(percent, total) {\n                    const p = Math.floor(percent * 100 * 0.99);\n                    if (lastPercent != p) {\n                        lastPercent = p;\n                        // console.log('ManagerPluginStore.install.downloadProgress', {p, total});\n                        Manager.sendBroadcast(\n                            \"store\",\n                            \"PluginInstallProgress\",\n                            {\n                                pluginName: pluginName,\n                                percent: p,\n                                end: false,\n                            },\n                        );\n                        if (ManagerPluginStore.installingMap[pluginName]) {\n                            ManagerPluginStore.installingMap[\n                                pluginName\n                            ].percent = p;\n                        }\n                    }\n                },\n            });\n            // sleep 500\n            await new Promise((resolve) => setTimeout(resolve, 500));\n            // console.log('ManagerPluginStore.install.downloadEnd');\n            // console.log('ManagerPluginStore.install.start');\n            await ManagerPlugin.installFromFileOrDir(\n                tempFile,\n                PluginType.STORE,\n            );\n            // console.log('ManagerPluginStore.install.end');\n            AppsMain.toast(\n                t(\"plugin.installComplete\", {\n                    title: infoRes.data[\"config\"][\"title\"],\n                }),\n                {\n                    status: \"success\",\n                },\n            );\n            Manager.sendBroadcast(\"store\", \"PluginInstallProgress\", {\n                pluginName: pluginName,\n                percent: 100,\n                end: true,\n            });\n        } catch (e) {\n            throw mapError(e);\n        } finally {\n            delete this.installingMap[pluginName];\n        }\n    },\n    async publish(\n        pluginName: string,\n        option?: {\n            version?: string;\n        },\n    ) {\n        option = Object.assign(\n            {\n                version: null,\n            },\n            option,\n        );\n        const plugin = await Manager.getPlugin(pluginName);\n        if (!plugin) {\n            throw \"PluginNotExists\";\n        }\n        if (plugin.type !== PluginType.DIR) {\n            throw \"PluginNotPublishAble\";\n        }\n        if (plugin.version !== option[\"version\"]) {\n            throw \"PublishVersionNotMatch\";\n        }\n        if (!plugin.runtime.root) {\n            throw \"PluginNotPublishAble\";\n        }\n        const root = plugin.runtime.root;\n        const config = await Files.read(resolve(root, \"config.json\"), {\n            isDataPath: false,\n        });\n        if (!config) {\n            throw \"PluginFormatError:-9\";\n        }\n        let configJson = null;\n        try {\n            configJson = JSON.parse(config);\n        } catch (e) {}\n        if (!configJson) {\n            throw \"PluginFormatError:-10\";\n        }\n        if (configJson[\"name\"] !== pluginName) {\n            throw \"PluginFormatError:-11\";\n        }\n        const payload = {\n            plugin: pluginName,\n            version: option[\"version\"],\n            feature: null,\n            content: null,\n            package: null,\n        };\n        configJson[\"development\"] = configJson[\"development\"] || {};\n        if (configJson[\"development\"][\"env\"] === \"dev\") {\n            throw \"PluginEnvError\";\n        }\n        configJson[\"development\"][\"releaseDoc\"] =\n            configJson[\"development\"][\"releaseDoc\"] || \"release.md\";\n        const releaseDocPath = resolve(\n            root,\n            configJson[\"development\"][\"releaseDoc\"],\n        );\n        // console.log('releaseDocPath', releaseDocPath)\n        const releaseDoc = await Files.read(releaseDocPath, {\n            isDataPath: false,\n        });\n        if (releaseDoc) {\n            const parts = releaseDoc.split(\"---\");\n            for (const part of parts) {\n                let lines = part.split(\"\\n\");\n                while (!payload.feature && lines.length) {\n                    const line = lines.shift();\n                    // ## x.x.x 功能特性\n                    if (line.startsWith(\"##\")) {\n                        const parts = line.split(\" \");\n                        if (parts.length === 3) {\n                            if (parts[1] === payload.version) {\n                                payload.feature = parts[2];\n                                payload.content = MarkdownUtil.toHtml(\n                                    lines.join(\"\\n\").trim(),\n                                );\n                                break;\n                            }\n                        }\n                    }\n                }\n                if (payload.feature) {\n                    break;\n                }\n            }\n        }\n        if (!payload.feature || !payload.content) {\n            if (!releaseDoc) {\n                throw \"PluginReleaseDocNotFound\";\n            }\n            if (!payload.feature) {\n                throw \"PluginReleaseDocFormatError:-1\";\n            }\n            throw \"PluginReleaseDocFormatError:-2\";\n        }\n        const pluginInfo = await this._getPluginInfo(root, configJson);\n        const tempFile = await Files.temp(\"zip\");\n        await Misc.zip(tempFile, plugin.runtime.root, {\n            filter: async (entry) => {\n                if (entry.isDir) {\n                    if ([\"node_modules\", \".git\"].includes(entry.name)) {\n                        return false;\n                    }\n                    if (\n                        await Files.exists(resolve(entry.fullPath, \".faignore\"))\n                    ) {\n                        return false;\n                    }\n                }\n                return true;\n            },\n            end: async (archive: any) => {\n                delete configJson[\"development\"];\n                delete configJson[\"$schema\"];\n                archive.append(JSON.stringify(configJson, null, 4), {\n                    name: \"config.json\",\n                });\n            },\n        });\n        if (!fs.existsSync(tempFile)) {\n            throw \"PluginZipError\";\n        }\n        // console.log('tempFile', tempFile)\n        const buffer = await Files.readBuffer(tempFile, {\n            isDataPath: false,\n        });\n        payload.package = buffer.toString(\"base64\");\n        await Files.deletes(tempFile, {\n            isDataPath: false,\n        });\n        return await UserApi.post(\"store/plugin_publish\", {\n            ...payload,\n            ...pluginInfo,\n        });\n    },\n    async publishInfo(\n        pluginName: string,\n        option?: {\n            version?: string;\n        },\n    ) {\n        option = Object.assign(\n            {\n                version: null,\n            },\n            option,\n        );\n        const plugin = await Manager.getPlugin(pluginName);\n        if (!plugin) {\n            throw \"PluginNotExists\";\n        }\n        if (plugin.type !== PluginType.DIR) {\n            throw \"PluginNotPublishAble\";\n        }\n        if (plugin.version !== option[\"version\"]) {\n            throw \"PublishVersionNotMatch\";\n        }\n        if (!plugin.runtime.root) {\n            throw \"PluginNotPublishAble\";\n        }\n        const root = plugin.runtime.root;\n        const config = await Files.read(resolve(root, \"config.json\"), {\n            isDataPath: false,\n        });\n        if (!config) {\n            throw \"PluginFormatError:-12\";\n        }\n        let configJson = null;\n        try {\n            configJson = JSON.parse(config);\n        } catch (e) {}\n        if (!configJson) {\n            throw \"PluginFormatError:-13\";\n        }\n        const payload = {\n            plugin: pluginName,\n            version: option[\"version\"],\n        };\n        const pluginInfo = await this._getPluginInfo(root, configJson);\n        return await UserApi.post(\"store/plugin_publish_info\", {\n            ...payload,\n            ...pluginInfo,\n        });\n    },\n    async storeInstallingInfo(pluginName: string) {\n        const result = {\n            isInstalling: false,\n            percent: 0,\n        };\n        if (this.installingMap[pluginName]) {\n            result.isInstalling = true;\n            result.percent = this.installingMap[pluginName].percent;\n        }\n        return result;\n    },\n    async _getPluginInfo(root: string, configJson: any) {\n        const result = {\n            pluginContent: null,\n            pluginPreview: null,\n        };\n        configJson[\"development\"] = configJson[\"development\"] || {};\n        configJson[\"development\"][\"contentDoc\"] =\n            configJson[\"development\"][\"contentDoc\"] || \"content.md\";\n        const contentDocPath = resolve(\n            root,\n            configJson[\"development\"][\"contentDoc\"],\n        );\n        const contentDoc = await Files.read(contentDocPath, {\n            isDataPath: false,\n        });\n        if (contentDoc) {\n            result.pluginContent = MarkdownUtil.toHtml(contentDoc);\n        }\n        configJson[\"development\"][\"previewDoc\"] =\n            configJson[\"development\"][\"previewDoc\"] || \"preview.md\";\n        const previewDocPath = resolve(\n            root,\n            configJson[\"development\"][\"previewDoc\"],\n        );\n        const previewDoc = await Files.read(previewDocPath, {\n            isDataPath: false,\n        });\n        if (previewDoc) {\n            const images = [];\n            previewDoc.split(\"\\n\").forEach((line: string) => {\n                // https://example.com/path/to/image.png\n                // ![image](https://example.com/path/to/image.png)\n                const match = line.match(/!\\[.*?\\]\\((.*?)\\)/);\n                if (match) {\n                    images.push(match[1].trim());\n                } else {\n                    images.push(line.trim());\n                }\n            });\n            result.pluginPreview = JSON.stringify(\n                images.filter((url) => !!url),\n            );\n        }\n        return result;\n    },\n};\n\n// setTimeout(() => {\n//     ManagerPluginStore.publishInfo('AxxxdDddd', {\n//         version: '1.2.0',\n//     })\n// }, 3000)\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/store.ts",
    "content": "import { ActionTypeEnum, PluginRecord } from \"../../../../../src/types/Manager\";\nimport { t } from \"../../../../config/lang\";\nimport { SystemIcons } from \"../asset/icon\";\n\nexport const StorePlugin: PluginRecord = {\n    name: \"store\",\n    title: t(\"plugin.market\"),\n    version: \"1.0.0\",\n    logo: SystemIcons.pluginStore,\n    description: t(\"system.storeDesc\"),\n    main: \"<root>/page/store.html\",\n    preload: \"<system>\",\n    actions: [\n        {\n            name: \"default\",\n            title: t(\"plugin.market\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.pluginStore,\n            matches: [t(\"plugin.market\"), \"store\"] as any,\n        },\n    ],\n};\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/system/action.ts",
    "content": "import os from \"os\";\nimport { ActionTypeCodeData } from \"../../../../../../src/types/Manager\";\nimport { t } from \"../../../../../config/lang\";\nimport { isLinux, isMac, isWin } from \"../../../../../lib/env\";\nimport { Page } from \"../../../../../page\";\nimport { AppsMain } from \"../../../../app/main\";\nimport { AppRuntime } from \"../../../../env\";\nimport { KeyboardKey, ManagerHotkeySimulate } from \"../../../hotkey/simulate\";\nimport { colorPicker } from \"../../../plugin/colorPicker\";\nimport { screenCapture } from \"../../../plugin/screenCapture\";\nimport { screenRecord } from \"../../../plugin/screenRecord\";\n\nexport const SystemActionCode = {\n    screenshot: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        screenCapture((image: string) => {\n            AppsMain.setClipboardImage(image);\n        });\n    },\n    colorPicker: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        colorPicker().then();\n    },\n    screenRecord: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        screenRecord().then();\n    },\n    guide: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        await Page.open(\"guide\", {});\n    },\n    about: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        await Page.open(\"about\", {});\n    },\n    lock: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        if (isMac) {\n            ManagerHotkeySimulate.keyTap(KeyboardKey.Q, [\n                KeyboardKey.Meta,\n                KeyboardKey.Ctrl,\n            ]);\n        } else if (isWin) {\n            ManagerHotkeySimulate.keyTap(KeyboardKey.L, [KeyboardKey.Meta]);\n        } else if (isLinux) {\n            ManagerHotkeySimulate.keyTap(KeyboardKey.L, [KeyboardKey.Meta]);\n        }\n    },\n    ip: async (data: ActionTypeCodeData) => {\n        AppRuntime.mainWindow.hide();\n        const ip = getLocalIPAddress();\n        AppsMain.setClipboardText(ip);\n        AppsMain.toast(t(\"system.ipCopied\", { ip }));\n    },\n};\n\nfunction getLocalIPAddress() {\n    const networkInterfaces = os.networkInterfaces();\n    for (const interfaceName in networkInterfaces) {\n        const interfaces = networkInterfaces[interfaceName];\n        for (const iface of interfaces) {\n            if (iface.family === \"IPv4\" && !iface.internal) {\n                return iface.address;\n            }\n        }\n    }\n    return \"127.0.0.1\";\n}\n"
  },
  {
    "path": "electron/mapi/manager/system/plugin/system.ts",
    "content": "import { ActionTypeEnum, PluginRecord } from \"../../../../../src/types/Manager\";\nimport { t } from \"../../../../config/lang\";\nimport { SystemIcons } from \"../asset/icon\";\n\nexport const SystemPlugin: PluginRecord = {\n    name: \"system\",\n    title: t(\"system.title\"),\n    version: \"1.0.0\",\n    logo: SystemIcons.pluginSystem,\n    description: t(\"system.desc\"),\n    main: \"<root>/page/system.html\",\n    preload: \"<system>\",\n    actions: [\n        {\n            name: \"page-data\",\n            title: t(\"system.dataCenter\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.database,\n            matches: [t(\"system.dataCenter\"), \"data\"] as any,\n        },\n        {\n            name: \"page-setting\",\n            title: t(\"system.functionSettings\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.pluginSystem,\n            matches: [t(\"system.functionSettings\"), \"setting\"] as any,\n        },\n        {\n            name: \"page-plugin\",\n            title: t(\"system.pluginManagement\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.plugin,\n            matches: [t(\"system.pluginManagement\"), \"plugin\"] as any,\n        },\n        {\n            name: \"page-action\",\n            title: t(\"system.actionManagement\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.command,\n            matches: [t(\"system.actionManagement\"), \"action\"] as any,\n        },\n        {\n            name: \"page-file\",\n            title: t(\"system.fileLaunch\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.folder,\n            matches: [t(\"system.fileLaunch\"), \"file\"] as any,\n        },\n        {\n            name: \"page-launch\",\n            title: t(\"system.hotkeys\"),\n            type: ActionTypeEnum.WEB,\n            icon: SystemIcons.thunder,\n            matches: [t(\"system.hotkeys\"), \"launch\"] as any,\n        },\n        {\n            name: \"about\",\n            title: t(\"system.about\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.about,\n            matches: [t(\"system.about\"), \"about\"] as any,\n        },\n        {\n            name: \"screenshot\",\n            title: t(\"system.screenshot\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.screenshot,\n            matches: [t(\"system.screenshot\"), \"screenshot\", \"snapshot\"] as any,\n        },\n        {\n            name: \"colorPicker\",\n            title: t(\"system.colorPicker\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.colorPicker,\n            matches: [t(\"system.colorPicker\"), \"ColorPicker\"] as any,\n        },\n        {\n            name: \"screenRecord\",\n            title: t(\"system.screenRecord\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.screenRecord,\n            matches: [t(\"system.screenRecord\"), \"ScreenRecord\"] as any,\n        },\n        {\n            name: \"guide\",\n            title: t(\"nav.guide\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.guide,\n            matches: [t(\"nav.guide\"), \"guide\"] as any,\n        },\n        {\n            name: \"lock\",\n            title: t(\"system.lockScreen\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.lock,\n            matches: [t(\"system.lockScreen\"), \"lock\"] as any,\n        },\n        {\n            name: \"ip\",\n            title: t(\"system.lanIP\"),\n            type: ActionTypeEnum.CODE,\n            icon: SystemIcons.ip,\n            matches: [t(\"system.lanIP\")] as any,\n        },\n    ],\n};\n"
  },
  {
    "path": "electron/mapi/manager/type.ts",
    "content": "import { BrowserView, BrowserWindow } from \"electron\";\nimport { ActiveWindow, PluginRecord } from \"../../../src/types/Manager\";\n\nexport type PluginContext = (BrowserView | {}) & {\n    _plugin: PluginRecord;\n    _window?: BrowserWindow;\n    _event?: {\n        [key: string]: any[];\n    };\n};\n\nexport type SearchQuery = {\n    keywords: string;\n    currentFiles?: FileItem[];\n    currentImage?: string;\n    currentText?: string;\n    activeWindow?: ActiveWindow;\n};\n"
  },
  {
    "path": "electron/mapi/manager/window/index.ts",
    "content": "import * as remoteMain from \"@electron/remote/main\";\nimport {\n    BrowserView,\n    BrowserWindow,\n    screen,\n    shell,\n    WebContents,\n} from \"electron\";\nimport {\n    ActionRecord,\n    PluginRecord,\n    PluginState,\n} from \"../../../../src/types/Manager\";\nimport { t } from \"../../../config/lang\";\nimport { WindowConfig } from \"../../../config/window\";\nimport { DevToolsManager } from \"../../../lib/devtools\";\nimport { isMac } from \"../../../lib/env\";\nimport {\n    preloadDefault,\n    rendererIsUrl,\n    rendererLoadPath,\n} from \"../../../lib/env-main\";\nimport { HotKeyUtil } from \"../../../lib/util\";\nimport { AppsMain } from \"../../app/main\";\nimport { AppEnv, AppRuntime } from \"../../env\";\nimport { Events } from \"../../event/main\";\nimport { Log } from \"../../log/main\";\nimport {\n    executeDarkMode,\n    executeHooks,\n    executePluginHooks,\n} from \"../lib/hooks\";\nimport { ManagerPlugin } from \"../plugin\";\nimport { ManagerPluginEvent } from \"../plugin/event\";\nimport { PluginLog } from \"../plugin/log\";\nimport { ManagerSystem } from \"../system\";\nimport { PluginContext } from \"../type\";\nimport { RemoteWebManager } from \"./remoteWeb\";\n\nconst browserViews = new Map<WebContents, BrowserView>();\nconst detachWindows = new Map<WebContents, BrowserWindow>();\nlet mainWindowView: BrowserView | null = null;\nconst mainPluginActionCode = {\n    view: null as BrowserView | null,\n    action: null as ActionRecord | null,\n    codeData: null,\n    items: [] as {\n        id: string;\n        [key: string]: any;\n    }[],\n};\n\ntype OpenOptionType = {\n    type: \"action\" | \"callPage\";\n    callPage?: {\n        type: string;\n        data: any;\n        option: CallPageOption;\n        onResult: (result: { code: number; msg: string; data?: any }) => void;\n    };\n};\n\ntype OpenShowWindowOption = {\n    loadUrl: () => void;\n    pluginState: PluginState;\n    width: number;\n    height: number;\n    option: OpenOptionType;\n};\n\nconst addBrowserViews = (view: BrowserView) => {\n    browserViews.set(view.webContents, view);\n};\n\nconst removeBrowserViews = (view: BrowserView) => {\n    browserViews.delete(view.webContents);\n};\n\nconst addDetachWindows = (win: BrowserWindow) => {\n    detachWindows.set(win.webContents, win);\n};\n\nconst removeDetachWindows = (win: BrowserWindow) => {\n    detachWindows.delete(win.webContents);\n};\n\nconst checkForHotkey = async (view: PluginContext, input: Electron.Input) => {\n    if (view._event && view._event[\"Hotkey\"]) {\n        const hotkey = HotKeyUtil.getFromEvent(input);\n        if (hotkey) {\n            view._event[\"Hotkey\"].forEach(({ id, hotkeys }) => {\n                if (HotKeyUtil.match(hotkeys, hotkey)) {\n                    executePluginHooks(view as BrowserView, \"Hotkey\", {\n                        id,\n                        hotkey,\n                    });\n                }\n            });\n        }\n    }\n};\n\nexport const ManagerWindow = {\n    listBrowserViews(): BrowserView[] {\n        return Array.from(browserViews.values());\n    },\n    listDetachWindows(): BrowserWindow[] {\n        return Array.from(detachWindows.values());\n    },\n    getViewByWebContents: (webContents: any) => {\n        // console.log('getViewByWebContents.value', webContents)\n        let view = browserViews.get(webContents);\n        if (view) {\n            return view;\n        }\n        const iterator = browserViews.entries();\n        while (true) {\n            const { value, done } = iterator.next();\n            if (done) {\n                break;\n            }\n            // console.log('getViewByWebContents.value.start', value[1], value[1]._window)\n            if (value[1]._window.webContents === webContents) {\n                return value[1];\n            }\n        }\n        return null;\n    },\n    async detachWindowOperate(type: \"open\" | \"close\", action: ActionRecord) {\n        let win = null;\n        for (const w of ManagerWindow.listDetachWindows()) {\n            if (w.id === action.runtime.windowId) {\n                win = w;\n                break;\n            }\n        }\n        if (!win) {\n            throw \"DetachWindowNotFound\";\n        }\n        if (type === \"open\") {\n            win.show();\n            win.focus();\n        } else {\n            win.close();\n        }\n        AppRuntime.mainWindow.setSize(\n            WindowConfig.mainWidth,\n            WindowConfig.mainHeight,\n        );\n        setTimeout(() => {\n            AppRuntime.mainWindow.hide();\n        }, 100);\n    },\n    async _logPluginViewError(view: BrowserView, plugin: PluginRecord) {\n        view.webContents.on(\n            \"did-fail-load\",\n            (event, errorCode, errorDescription, validatedURL) => {\n                PluginLog.error(plugin.name, \"Load.Error-did-fail-load\", {\n                    errorCode,\n                    errorDescription,\n                    validatedURL,\n                });\n            },\n        );\n        view.webContents.on(\n            \"did-fail-provisional-load\",\n            (event, errorCode, errorDescription, validatedURL) => {\n                PluginLog.error(\n                    plugin.name,\n                    \"Load.Error-did-fail-provisional-load\",\n                    {\n                        errorCode,\n                        errorDescription,\n                        validatedURL,\n                    },\n                );\n            },\n        );\n        view.webContents.on(\"preload-error\", (event, preloadPath, error) => {\n            PluginLog.error(plugin.name, \"Load.Error-preload-error\", {\n                error: error + \"\",\n                preloadPath,\n            });\n        });\n        view.webContents.on(\"render-process-gone\", () => {\n            PluginLog.error(plugin.name, \"Load.Error-render-process-gone\", {\n                error: \"render-process-gone\",\n            });\n        });\n    },\n    async _pluginViewLoad(view: BrowserView, main: string) {\n        try {\n            if (rendererIsUrl(main)) {\n                await view.webContents.loadURL(main);\n            } else {\n                await view.webContents.loadFile(main);\n            }\n        } catch (e) {\n            view.webContents.loadURL(\"about:blank\").then();\n            PluginLog.error(view._plugin.name, \"Load.Error-loadUrl\", {\n                error: e + \"\",\n                main,\n            });\n        }\n    },\n    async _pluginActionCodeEnd() {\n        if (mainPluginActionCode.view) {\n            AppRuntime.mainWindow.removeBrowserView(mainPluginActionCode.view);\n            removeBrowserViews(mainPluginActionCode.view);\n            if (\n                ManagerPlugin.isDevelopmentCheck(\n                    mainPluginActionCode.view._plugin,\n                    \"keepCodeDevTools\",\n                )\n            ) {\n                PluginLog.info(\n                    mainPluginActionCode.view._plugin.name,\n                    \"ManagerWindow.KeepCodeDevTools\",\n                    {\n                        action: mainPluginActionCode.action,\n                        codeData: mainPluginActionCode.codeData,\n                    },\n                );\n            } else {\n                // @ts-ignore\n                mainPluginActionCode.view.webContents?.destroy();\n                mainPluginActionCode.view = null;\n            }\n        }\n        mainPluginActionCode.action = null;\n        mainPluginActionCode.codeData = null;\n        mainPluginActionCode.items = [];\n    },\n    async _viewCodeCallJs(js: string) {\n        return await mainPluginActionCode.view.webContents.executeJavaScript(\n            `(async()=>{ ${js} })();`,\n        );\n    },\n    async actionCodeExecute(\n        id: string | null = null,\n        keywords: string | null = null,\n    ) {\n        let item: ActionCodeExecuteResultItem | null = null;\n        if (id) {\n            item = mainPluginActionCode.items.find(\n                (i) => i.id === id,\n            ) as ActionCodeExecuteResultItem;\n        }\n        try {\n            let hasLoading = false;\n            if (!(item && \"loading\" in item && !item[\"loading\"])) {\n                await executeHooks(AppRuntime.mainWindow, \"PluginCodeSetting\", {\n                    loading: true,\n                });\n                hasLoading = true;\n            }\n            let value: ActionCodeExecuteResult = await this._viewCodeCallJs(\n                `return await window.exports.code['${mainPluginActionCode.action.name}'].execute(\n                    ${JSON.stringify(item)},\n                    ${JSON.stringify(keywords)},\n                    ${JSON.stringify(mainPluginActionCode.codeData)}\n                );`,\n            );\n            if (!value) {\n                value = { command: \"none\" } as ActionCodeExecuteResult;\n            }\n            if (hasLoading) {\n                await executeHooks(AppRuntime.mainWindow, \"PluginCodeSetting\", {\n                    loading: false,\n                });\n            }\n            // console.log('ManagerWindow.openActionCode.value', JSON.stringify(value))\n            const plugin: PluginRecord = mainPluginActionCode.view._plugin;\n            if (value.placeholder) {\n                await executeHooks(AppRuntime.mainWindow, \"PluginCodeSetting\", {\n                    placeholder: value.placeholder,\n                });\n            }\n            if (\"data\" === value.command) {\n                mainPluginActionCode.items = value.items || [];\n                // icon path\n                mainPluginActionCode.items.forEach((item) => {\n                    if (\n                        item.icon &&\n                        !item.icon.startsWith(\"http:\") &&\n                        !item.icon.startsWith(\"file:\") &&\n                        !item.icon.startsWith(\"data:\")\n                    ) {\n                        item.icon = `file://${plugin.runtime.root}/${item.icon}`;\n                    }\n                });\n                await executeHooks(AppRuntime.mainWindow, \"PluginCodeData\", {\n                    items: value.items,\n                });\n            } else if (\"close\" === value.command) {\n                await this.close();\n                AppRuntime.mainWindow.hide();\n            } else if (\"error\" === value.command) {\n                await executeHooks(AppRuntime.mainWindow, \"PluginCodeSetting\", {\n                    error: value.error,\n                });\n            } else if (\"clear\" === value.command) {\n                await this.close();\n            } else if (\"none\" === value.command) {\n                // do nothing\n            } else {\n                throw `ManagerWindow.OpenActionCode.CommandError:${value.command}`;\n            }\n        } catch (e) {\n            await executeHooks(AppRuntime.mainWindow, \"PluginCodeSetting\", {\n                error: e + \"\",\n            });\n            PluginLog.error(\n                mainPluginActionCode.view._plugin.name,\n                \"Code.Error\",\n                {\n                    error: e + \"\",\n                    action: mainPluginActionCode.action,\n                },\n            );\n        }\n    },\n    async openForCode(\n        plugin: PluginRecord,\n        action: ActionRecord,\n        option?: {\n            codeData?: any;\n        },\n    ) {\n        const { nodeIntegration, preloadBase, preload, main } =\n            await ManagerPlugin.getInfo(plugin);\n        // console.log('openForCode', {preload, main})\n        const viewSession = await ManagerPlugin.getViewSession(plugin);\n        if (preloadBase) {\n            viewSession.setPreloads([preloadBase]);\n        }\n        const view = new BrowserView({\n            webPreferences: {\n                webSecurity: false,\n                nodeIntegration,\n                contextIsolation: false,\n                sandbox: false,\n                devTools: true,\n                webviewTag: true,\n                preload,\n                session: viewSession,\n                defaultFontSize: 14,\n                defaultFontFamily: {\n                    standard: \"system-ui\",\n                    serif: \"system-ui\",\n                },\n                spellcheck: false,\n            },\n        });\n        mainPluginActionCode.view = view;\n        mainPluginActionCode.action = action;\n        mainPluginActionCode.codeData = option?.codeData || null;\n        await ManagerWindow._logPluginViewError(view, plugin);\n        addBrowserViews(view);\n        view._plugin = plugin;\n        view._window = AppRuntime.mainWindow;\n        remoteMain.enable(view.webContents);\n        AppRuntime.mainWindow.addBrowserView(view);\n        ManagerWindow._pluginViewLoad(view, main).then();\n        DevToolsManager.register(`MainCodeView.${plugin.name}`, view);\n        const logPluginError = (e) => {\n            PluginLog.error(plugin.name, \"Code.Error\", {\n                error: e + \"\",\n                action,\n                option,\n            });\n        };\n        const endView = () => {\n            setTimeout(() => {\n                this._pluginActionCodeEnd();\n            }, 1000);\n            AppRuntime.mainWindow.hide();\n        };\n        AppRuntime.mainWindow.setSize(\n            WindowConfig.pluginWidth,\n            WindowConfig.mainHeight,\n        );\n        return new Promise((resolve, reject) => {\n            view.webContents.once(\"dom-ready\", async () => {\n                DevToolsManager.autoShow(view);\n                if (\n                    ManagerPlugin.isDevelopmentCheck(plugin, \"showCodeDevTools\")\n                ) {\n                    view.webContents.openDevTools({\n                        mode: \"detach\",\n                        activate: true,\n                        title: `MainPluginCodeView.${plugin.name}`,\n                    });\n                }\n                view.setBounds({\n                    x: 0,\n                    y: 0,\n                    width: 0,\n                    height: 0,\n                });\n                try {\n                    const codeType = await this._viewCodeCallJs(\n                        `return typeof window.exports.code['${action.name}'];`,\n                    );\n                    if (\"function\" === codeType) {\n                        const value = await this._viewCodeCallJs(\n                            `return await window.exports.code['${action.name}'](${JSON.stringify(mainPluginActionCode.codeData)});`,\n                        );\n                        resolve(value);\n                        endView();\n                    } else {\n                        const codeSetting = await this._viewCodeCallJs(\n                            `return window.exports.code['${action.name}'].setting;`,\n                        );\n                        if (!codeSetting) {\n                            throw `ManagerWindow.OpenForCode.SettingEmpty`;\n                        }\n                        await executeHooks(\n                            AppRuntime.mainWindow,\n                            \"PluginCodeInit\",\n                            {\n                                plugin: plugin,\n                                type: codeSetting.type || \"list\",\n                                placeholder:\n                                    codeSetting.placeholder ||\n                                    t(\"store.searchPlaceholder\"),\n                            },\n                        );\n                        this.actionCodeExecute().then();\n                        resolve(null);\n                    }\n                } catch (e) {\n                    logPluginError(e);\n                    reject(e);\n                    endView();\n                }\n            });\n        });\n    },\n    async open(\n        plugin: PluginRecord,\n        action?: ActionRecord,\n        option?: OpenOptionType,\n    ) {\n        option = Object.assign(\n            {\n                type: \"action\",\n                callPage: {},\n            },\n            option,\n        );\n        const {\n            nodeIntegration,\n            preloadBase,\n            preload,\n            main,\n            width,\n            height,\n            autoDetach,\n            singleton,\n            zoom,\n        } = await ManagerPlugin.getInfo(plugin);\n        // console.log('ManagerWindow.open', {nodeIntegration, preload, main, width, height, autoDetach})\n        const readyData = {};\n        readyData[\"actionName\"] = action?.name || null;\n        readyData[\"actionMatch\"] = action?.runtime?.match || null;\n        readyData[\"actionMatchFiles\"] = action?.runtime?.matchFiles || [];\n        readyData[\"requestId\"] = action?.runtime?.requestId || null;\n        readyData[\"reenter\"] = false;\n        readyData[\"isView\"] = false;\n        readyData[\"type\"] = option.type;\n        if (option.type === \"action\" && singleton) {\n            for (const v of this.listBrowserViews()) {\n                if (v._plugin.name === plugin.name) {\n                    v._window.show();\n                    v._window.focus();\n                    await executeHooks(\n                        AppRuntime.mainWindow,\n                        \"PluginAlreadyOpened\",\n                        {},\n                    );\n                    readyData[\"reenter\"] = true;\n                    await executePluginHooks(v, \"PluginReady\", readyData);\n                    return;\n                }\n            }\n        }\n        const viewSession = await ManagerPlugin.getViewSession(plugin);\n        if (preloadBase) {\n            viewSession.setPreloads([preloadBase]);\n        }\n        if (plugin.setting.remoteWebCacheEnable) {\n            await RemoteWebManager.create(plugin);\n        }\n        // console.log('preload', {preloadPluginDefault, preload})\n        const view = new BrowserView({\n            webPreferences: {\n                webSecurity: false,\n                nodeIntegration,\n                contextIsolation: false,\n                allowRunningInsecureContent: true,\n                sandbox: false,\n                devTools: true,\n                webviewTag: true,\n                preload,\n                session: viewSession,\n                defaultFontSize: 14,\n                defaultFontFamily: {\n                    standard: \"system-ui\",\n                    serif: \"system-ui\",\n                },\n                spellcheck: false,\n            },\n        });\n        await ManagerWindow._logPluginViewError(view, plugin);\n        addBrowserViews(view);\n        view._plugin = plugin;\n        remoteMain.enable(view.webContents);\n        DevToolsManager.register(`PluginView.${plugin.name}`, view);\n        view.webContents.once(\"dom-ready\", async () => {\n            await executeDarkMode(view, {\n                plugin,\n                isSystem: ManagerSystem.match(plugin.name),\n            });\n            Events.sendRaw(view.webContents, \"APP_READY\", {\n                name: plugin.name,\n                AppEnv,\n            });\n        });\n        view.webContents.once(\"did-frame-finish-load\", () => {\n            // console.log('setZoomFactor', zoom / 100)\n            setTimeout(() => {\n                view.webContents.setZoomFactor(zoom / 100);\n            }, 0);\n        });\n        view.webContents.setWindowOpenHandler(({ url }) => {\n            if (url.startsWith(\"https://\") || url.startsWith(\"http://\")) {\n                shell.openExternal(url);\n            }\n            return { action: \"deny\" };\n        });\n        view.setAutoResize({ width: true, height: true });\n        // console.log('ManagerWindow.open', {nodeIntegration, preload, main, width, height, autoDetach})\n        view.webContents.once(\"dom-ready\", async () => {\n            DevToolsManager.autoShow(view);\n            if (ManagerPlugin.isDevelopmentCheck(plugin, \"showDevTools\")) {\n                view.webContents.openDevTools({\n                    mode: \"detach\",\n                    activate: true,\n                    title: `PluginView.${plugin.name}`,\n                });\n            }\n            if (option.type === \"callPage\") {\n                Events.callPage(\n                    view.webContents,\n                    option.callPage.type,\n                    option.callPage.data,\n                    option.callPage.option,\n                )\n                    .then((result) => {\n                        option.callPage.onResult(result);\n                    })\n                    .catch((e) => {\n                        option.callPage.onResult({ code: -1, msg: e + \"\" });\n                    })\n                    .finally(() => {\n                        if (option.callPage.option.autoClose) {\n                            setTimeout(() => {\n                                view._window.close();\n                            }, 1000);\n                        }\n                    });\n                readyData[\"isView\"] = true;\n            }\n        });\n        view.webContents.on(\"before-input-event\", (event, input) => {\n            // console.log('Load.Error-before-input-event', input)\n            if (input.type === \"keyUp\") {\n                // exit when Escape key is pressed\n                if (mainWindowView === view) {\n                    if (input.key === \"Escape\") {\n                        if (\n                            !(\n                                input.meta ||\n                                input.control ||\n                                input.shift ||\n                                input.alt\n                            )\n                        ) {\n                            if (mainWindowView) {\n                                ManagerWindow.close();\n                                AppRuntime.mainWindow.webContents.focus();\n                            }\n                        }\n                    }\n                } else {\n                    if (input.key === \"Escape\") {\n                        if (\n                            !(\n                                input.meta ||\n                                input.control ||\n                                input.shift ||\n                                input.alt\n                            )\n                        ) {\n                            view._window.isFullScreen() &&\n                                view._window.setFullScreen(false);\n                        }\n                    }\n                }\n            } else if (input.type === \"keyDown\") {\n                checkForHotkey(view as any, input);\n            }\n        });\n        const windowOption: OpenShowWindowOption = {\n            width,\n            height,\n            pluginState: {\n                value: \"\",\n                placeholder: \"\",\n                isVisible: false,\n            },\n            loadUrl: async () => {\n                ManagerWindow._pluginViewLoad(view, main).then();\n            },\n            option,\n        };\n        setTimeout(async () => {\n            if (autoDetach) {\n                if (!mainWindowView) {\n                    AppRuntime.mainWindow.hide();\n                }\n            }\n            if (autoDetach || option.type === \"callPage\") {\n                await this._showInDetachWindow(view, windowOption);\n            } else {\n                await this._showInMainWindow(view, windowOption);\n            }\n            // Log.info('open.PluginReady', JSON.stringify({readyData, action}))\n            await executePluginHooks(view, \"PluginReady\", readyData);\n        }, 0);\n    },\n    async subInputChange(win: BrowserWindow, keywords: string) {\n        const views = win.getBrowserViews();\n        for (const view of views) {\n            if (AppRuntime.mainWindow === win && view !== mainWindowView) {\n                continue;\n            }\n            await executePluginHooks(view, \"SubInputChange\", keywords);\n        }\n    },\n    async close(\n        plugin?: PluginRecord,\n        option?: {\n            window?: BrowserWindow;\n            openForNext?: boolean;\n        },\n    ) {\n        option = Object.assign(\n            {\n                openForNext: false,\n            },\n            option,\n        );\n        if (\n            mainWindowView &&\n            (!plugin || mainWindowView._plugin.name === plugin.name)\n        ) {\n            await executePluginHooks(mainWindowView, \"PluginExit\", null).then();\n            await executeHooks(AppRuntime.mainWindow, \"PluginExit\", {\n                openForNext: option.openForNext,\n            });\n            removeBrowserViews(mainWindowView);\n            AppRuntime.mainWindow.removeBrowserView(mainWindowView);\n            // @ts-ignore\n            mainWindowView.webContents?.destroy();\n            mainWindowView = null;\n        } else if (\n            mainPluginActionCode.view &&\n            (!plugin || mainPluginActionCode.view._plugin.name === plugin.name)\n        ) {\n            await executeHooks(AppRuntime.mainWindow, \"PluginCodeExit\", {});\n            await this._pluginActionCodeEnd();\n        } else {\n            // detach的插件窗口\n            if (option.window) {\n                option.window.close();\n            } else {\n                Log.error(\"ManagerWindow.close\", \"windowNotFound\");\n            }\n        }\n    },\n    async openMainPluginDevTools(option?: {}) {\n        const devToolsWin = DevToolsManager.getWindow(mainWindowView);\n        if (devToolsWin) {\n            devToolsWin.close();\n        } else if (mainWindowView) {\n            if (mainWindowView.webContents.isDevToolsOpened()) {\n                mainWindowView.webContents.closeDevTools();\n            } else {\n                mainWindowView.webContents.openDevTools({\n                    mode: \"detach\",\n                    activate: true,\n                    title: `MainPluginView`,\n                });\n            }\n        } else if (mainPluginActionCode.view) {\n            if (mainPluginActionCode.view.webContents.isDevToolsOpened()) {\n                mainPluginActionCode.view.webContents.closeDevTools();\n            } else {\n                mainPluginActionCode.view.webContents.openDevTools({\n                    mode: \"detach\",\n                    activate: true,\n                    title: `MainPluginCodeView.${mainPluginActionCode.view._plugin.name}`,\n                });\n            }\n        } else {\n            Log.error(\n                \"ManagerWindow.openMainPluginDevTools\",\n                \"mainWindowViewNotFound\",\n            );\n        }\n    },\n    async _showInMainWindow(view: BrowserView, option: OpenShowWindowOption) {\n        if (!(await ManagerPluginEvent.isMainWindowShown(null, null))) {\n            await ManagerPluginEvent.showMainWindow(null, null);\n        }\n        // console.log('showInMainWindow', view._plugin.name, option)\n        if (mainWindowView) {\n            await this.close(mainWindowView._plugin, {\n                openForNext: true,\n            });\n            mainWindowView = null;\n        }\n        view._window = AppRuntime.mainWindow;\n        mainWindowView = view;\n        AppRuntime.mainWindow.addBrowserView(view);\n        AppRuntime.mainWindow.setSize(\n            option.width,\n            WindowConfig.mainHeight + option.height,\n        );\n        const pluginParam = {};\n        const pluginState: PluginState = {\n            value: \"\",\n            placeholder: \"\",\n            isVisible: false,\n        };\n        const pluginInitReadyParam = {\n            plugin: view._plugin,\n            state: pluginState,\n            param: pluginParam,\n        };\n        await executeHooks(view._window, \"PluginInit\", pluginInitReadyParam);\n        view.webContents.once(\"dom-ready\", async () => {\n            await executeHooks(\n                view._window,\n                \"PluginInitReady\",\n                pluginInitReadyParam,\n            );\n            view.setBounds({\n                x: 0,\n                y: WindowConfig.mainHeight,\n                width: option.width,\n                height: option.height,\n            });\n            AppRuntime.mainWindow.focus();\n        });\n        option.loadUrl();\n    },\n    async _showInDetachWindow(view: BrowserView, option: OpenShowWindowOption) {\n        const plugin = view._plugin;\n        let alwaysOnTop = false;\n        if (plugin.setting?.detachAlwaysOnTop) {\n            alwaysOnTop = true;\n        }\n        const { x, y } = AppsMain.calcPositionInCurrentDisplay(\n            plugin.setting?.detachPosition || \"center\",\n            option.width,\n            option.height + WindowConfig.detachWindowTitleHeight,\n        );\n        let win = new BrowserWindow({\n            height: option.height + WindowConfig.detachWindowTitleHeight,\n            width: option.width,\n            autoHideMenuBar: true,\n            titleBarStyle: \"hidden\",\n            trafficLightPosition: { x: 10, y: 11 },\n            title: plugin.title,\n            resizable: true,\n            frame: false,\n            show: false,\n            transparent: false,\n            enableLargerThanScreen: true,\n            backgroundColor: \"#fff\",\n            alwaysOnTop,\n            x,\n            y,\n            center: true,\n            webPreferences: {\n                webSecurity: false,\n                allowRunningInsecureContent: true,\n                backgroundThrottling: false,\n                nodeIntegration: true,\n                contextIsolation: false,\n                webviewTag: true,\n                devTools: true,\n                navigateOnDragDrop: true,\n                spellcheck: false,\n                preload: preloadDefault,\n            },\n        });\n        win._name = `DetachWindow.${view._plugin.name}`;\n        win._plugin = view._plugin;\n        win._type = option.option.type;\n        view._window = win;\n        remoteMain.enable(win.webContents);\n        win.on(\"close\", () => {\n            executePluginHooks(view, \"PluginExit\", null);\n            removeBrowserViews(view);\n            removeDetachWindows(win);\n        });\n        win.on(\"closed\", async () => {\n            // @ts-ignore\n            view.webContents?.destroy();\n            win = undefined;\n            await executeHooks(AppRuntime.mainWindow, \"DetachWindowClosed\", {});\n        });\n        win.on(\"focus\", () => {\n            view && win.webContents?.focus();\n        });\n        DevToolsManager.register(`DetachWindow.${view._plugin.name}`, win);\n        win.on(\"maximize\", () => {\n            executeHooks(win, \"Maximize\");\n            const display = screen.getDisplayMatching(win.getBounds());\n            view.setBounds({\n                x: 0,\n                y: WindowConfig.detachWindowTitleHeight,\n                width: display.workArea.width,\n                height:\n                    display.workArea.height -\n                    WindowConfig.detachWindowTitleHeight,\n            });\n        });\n        win.on(\"unmaximize\", () => {\n            executeHooks(win, \"Unmaximize\");\n            const bounds = win.getBounds();\n            const display = screen.getDisplayMatching(bounds);\n            const width =\n                (display.scaleFactor * bounds.width) % 1 == 0\n                    ? bounds.width\n                    : bounds.width - 2;\n            const height =\n                (display.scaleFactor * bounds.height) % 1 == 0\n                    ? bounds.height\n                    : bounds.height - 2;\n            view.setBounds({\n                x: 0,\n                y: WindowConfig.detachWindowTitleHeight,\n                width,\n                height: height - WindowConfig.detachWindowTitleHeight,\n            });\n        });\n        win.webContents.once(\"render-process-gone\", () => {\n            // console.log('detach.render-process-gone')\n            win.close();\n        });\n        win.webContents.on(\"before-input-event\", (event, input) => {\n            if (input.type === \"keyDown\") {\n                checkForHotkey(view as any, input);\n            }\n        });\n        if (isMac) {\n            win.on(\"enter-full-screen\", () => {\n                executeHooks(win, \"EnterFullScreen\");\n            });\n            win.on(\"leave-full-screen\", () => {\n                executeHooks(win, \"LeaveFullScreen\");\n            });\n        }\n        win.webContents.on(\"will-navigate\", (event) => {\n            event.preventDefault();\n        });\n        win.webContents.setWindowOpenHandler(() => {\n            return { action: \"deny\" };\n        });\n        if (option.loadUrl) {\n            option.loadUrl();\n        }\n        const pluginJson = JSON.parse(JSON.stringify(view._plugin));\n        return new Promise((resolve, reject) => {\n            win.webContents.once(\"dom-ready\", async () => {\n                await executeDarkMode(win, {\n                    plugin,\n                    isSystem: true,\n                });\n                view.setAutoResize({ width: true, height: true });\n                win.setBrowserView(view);\n                view.setBounds({\n                    x: 0,\n                    y: WindowConfig.detachWindowTitleHeight,\n                    width: option.width,\n                    height: option.height,\n                });\n                DevToolsManager.autoShow(win);\n                const pluginParam = {\n                    alwaysOnTop,\n                };\n                await executeHooks(win, \"PluginInit\", {\n                    plugin: pluginJson,\n                    state: option.pluginState,\n                    param: pluginParam,\n                });\n                if (\n                    option.option.type === \"action\" ||\n                    (option.option.type === \"callPage\" &&\n                        option.option.callPage?.option.showWindow)\n                ) {\n                    win.show();\n                }\n                resolve(undefined);\n            });\n            rendererLoadPath(win, \"page/detachWindow.html\");\n            addDetachWindows(win);\n        });\n    },\n    async detach(option?: {}) {\n        if (!mainWindowView) {\n            throw \"MainViewNotFound\";\n        }\n        const pluginState: PluginState = await executeHooks(\n            AppRuntime.mainWindow,\n            \"PluginState\",\n        );\n        AppRuntime.mainWindow.removeBrowserView(mainWindowView);\n        const bounds = mainWindowView.getBounds();\n        await this._showInDetachWindow(mainWindowView, {\n            pluginState,\n            width: bounds.width,\n            height: bounds.height,\n            option: {\n                type: \"action\",\n            },\n        });\n        mainWindowView = null;\n        await executeHooks(AppRuntime.mainWindow, \"PluginDetached\");\n        AppRuntime.mainWindow.hide();\n    },\n    async toggleDetachPluginAlwaysOnTop(\n        view: BrowserView,\n        alwaysOnTop: boolean,\n        option?: {},\n    ) {\n        view._window.setAlwaysOnTop(alwaysOnTop);\n        return alwaysOnTop;\n    },\n    async setDetachPluginZoom(view: BrowserView, zoom: number, option?: {}) {\n        view.webContents.setZoomFactor(zoom / 100);\n    },\n    async firePluginMoreMenuClick(\n        view: BrowserView,\n        name: string,\n        option?: {},\n    ) {\n        await executePluginHooks(view, \"MoreMenuClick\", { name });\n    },\n    async fireDetachOperateClick(view: BrowserView, name: string, option?: {}) {\n        await executePluginHooks(view, \"DetachOperateClick\", { name });\n    },\n    async closeDetachPlugin(view: BrowserView, option?: {}) {\n        view._window.close();\n    },\n    async openDetachPluginDevTools(view: BrowserView, option?: {}) {\n        const devToolsWin = DevToolsManager.getWindow(view);\n        if (devToolsWin) {\n            devToolsWin.close();\n        } else if (view.webContents.isDevToolsOpened()) {\n            view.webContents.closeDevTools();\n        } else {\n            view.webContents.openDevTools({\n                mode: \"detach\",\n                activate: true,\n                title: `DetachView.${view._plugin.name}`,\n            });\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/manager/window/remoteWeb.ts",
    "content": "import { PluginRecord } from \"../../../../src/types/Manager\";\nimport { ManagerPlugin } from \"../plugin\";\nimport path from \"node:path\";\nimport { Files } from \"../../file/main\";\nimport { FileUtil } from \"../../../lib/util\";\nimport { PluginLog } from \"../plugin/log\";\n\ntype FileMeta = {\n    mimeType: string;\n    headers: Record<string, string>;\n};\n\nexport const RemoteWebManager = {\n    create: async (plugin: PluginRecord) => {\n        const shouldBlock = (url: string) => {\n            if (plugin.runtime.remoteWeb && plugin.runtime.remoteWeb.blocks) {\n                for (const block of plugin.runtime.remoteWeb.blocks) {\n                    if (block.startsWith(\"/\") && block.endsWith(\"/\")) {\n                        const regex = new RegExp(block.slice(1, -1));\n                        if (regex.test(url)) {\n                            return true;\n                        }\n                    } else {\n                        if (url.includes(block)) {\n                            return true;\n                        }\n                    }\n                }\n                return false;\n            }\n        };\n\n        const getFileMeta = async (file: string): Promise<FileMeta> => {\n            const defaultMeta: FileMeta = {\n                mimeType: \"application/octet-stream\",\n                headers: {},\n            };\n            const meta = file + \".meta.json\";\n            if (!(await Files.exists(meta, { isDataPath: false }))) {\n                return defaultMeta;\n            }\n            const content = await Files.read(meta, { isDataPath: false });\n            if (!content) {\n                return defaultMeta;\n            }\n            try {\n                const json: FileMeta = JSON.parse(content);\n                if (json) {\n                    return {\n                        mimeType: json.mimeType || defaultMeta.mimeType,\n                        headers: json.headers || defaultMeta.headers,\n                    };\n                }\n            } catch (e) {}\n            return defaultMeta;\n        };\n\n        const writeFileMeta = async (\n            file: string,\n            meta: FileMeta,\n        ): Promise<void> => {\n            const metaFile = file + \".meta.json\";\n            const content = JSON.stringify(meta, null, 2);\n            await Files.write(metaFile, content, { isDataPath: false });\n        };\n\n        const getCacheFile = (url: string, param: any = {}): string | null => {\n            if (!plugin.runtime.remoteWeb) {\n                return null;\n            }\n            const root = path.join(plugin.runtime.root, \"RemoteWebCache\");\n            if (plugin.runtime.remoteWeb.urlMap) {\n                if (plugin.runtime.remoteWeb.urlMap[url]) {\n                    return path.join(\n                        root,\n                        plugin.runtime.remoteWeb.urlMap[url],\n                    );\n                }\n            }\n            if (\n                !plugin.runtime.remoteWeb.types ||\n                !plugin.runtime.remoteWeb.domains\n            ) {\n                return null;\n            }\n            if (\n                !plugin.runtime.remoteWeb.types.length ||\n                !plugin.runtime.remoteWeb.domains.length\n            ) {\n                return null;\n            }\n            const urlInfo = new URL(url);\n            let ext = Files.ext(urlInfo.pathname);\n            if (!ext) {\n                return null;\n            }\n            if (!plugin.runtime.remoteWeb.types.includes(ext)) {\n                return null;\n            }\n            if (!plugin.runtime.remoteWeb.domains.includes(urlInfo.hostname)) {\n                return null;\n            }\n            let f = `${urlInfo.hostname}${urlInfo.pathname}`.replace(\n                /[^a-zA-Z0-9\\\\/.]/g,\n                \"_\",\n            );\n            if (urlInfo.search) {\n                f =\n                    f +\n                    \"-\" +\n                    urlInfo.search.replace(/[^a-zA-Z0-9\\\\/.]/g, \"_\") +\n                    \".\" +\n                    ext;\n            }\n            return path.join(root, f);\n        };\n\n        const webSession = await ManagerPlugin.getViewSession(\n            plugin,\n            \"RemoteWeb\",\n        );\n\n        if (!webSession.protocol.isProtocolHandled(\"https\")) {\n            const requestHandler = async (request): Promise<any> => {\n                const url = request.url;\n                const file = getCacheFile(url);\n                if (file && (await Files.exists(file, { isDataPath: false }))) {\n                    const buffer = await Files.readBuffer(file, {\n                        isDataPath: false,\n                    });\n                    const fileMeta = await getFileMeta(file);\n                    PluginLog.info(plugin.name, \"RemoteWeb.Cache.Hit\", {\n                        url,\n                    });\n                    return new Response(buffer, {\n                        status: 200,\n                        headers: {\n                            \"content-type\": fileMeta.mimeType,\n                            \"focusany-cache\": \"hit\",\n                            ...fileMeta.headers,\n                        },\n                    });\n                }\n                if (!file && shouldBlock(url)) {\n                    PluginLog.info(plugin.name, \"RemoteWeb.Cache.Blocked\", {\n                        url,\n                    });\n                    return new Response(`RemoteWebBlock - ${url}`, {\n                        status: 403,\n                        headers: { \"content-type\": \"text/plain\" },\n                    });\n                }\n                return new Promise((resolve, reject) => {\n                    fetch(url, {\n                        method: request.method || \"GET\",\n                        headers: {\n                            ...request.headers,\n                            \"User-Agent\":\n                                plugin.runtime.remoteWeb?.userAgent ||\n                                \"FocusAny RemoteWeb Manager\",\n                        },\n                    })\n                        .then(async (response) => {\n                            if (!response.ok) {\n                                PluginLog.error(\n                                    plugin.name,\n                                    \"RemoteWeb.Cache.FetchFailed\",\n                                    {\n                                        url,\n                                        status: response.status,\n                                        statusText: response.statusText,\n                                    },\n                                );\n                                return resolve(\n                                    new Response(\"Fetch failed: \" + url, {\n                                        status: response.status,\n                                        headers: {\n                                            \"content-type\": \"text/plain\",\n                                        },\n                                    }),\n                                );\n                            }\n                            const buffer = await response.arrayBuffer();\n                            const mimeType =\n                                response.headers.get(\"content-type\") ||\n                                FileUtil.getMimeByPath(\n                                    file,\n                                    \"application/octet-stream\",\n                                );\n                            const headers = {};\n                            response.headers.forEach((value, key) => {\n                                headers[key] = value;\n                            });\n                            const headerToDelete = [\n                                \"content-security-policy\",\n                                \"content-encoding\",\n                            ];\n                            for (const key of headerToDelete) {\n                                if (headers[key]) {\n                                    delete headers[key];\n                                }\n                            }\n                            let cacheStatus = \"miss\";\n                            if (file) {\n                                await Files.writeBuffer(\n                                    file,\n                                    Buffer.from(buffer),\n                                    { isDataPath: false },\n                                );\n                                await writeFileMeta(file, {\n                                    mimeType,\n                                    headers,\n                                });\n                                cacheStatus = \"cached\";\n                            }\n                            PluginLog.info(\n                                plugin.name,\n                                \"RemoteWeb.Cache.Write\",\n                                {\n                                    url,\n                                    mimeType,\n                                    headers,\n                                    cacheStatus,\n                                    length: buffer.byteLength,\n                                },\n                            );\n                            resolve(\n                                new Response(buffer, {\n                                    status: 200,\n                                    headers: {\n                                        \"content-type\": mimeType,\n                                        \"focusany-cache\": cacheStatus,\n                                        ...headers,\n                                    },\n                                }),\n                            );\n                        })\n                        .catch((err) => {\n                            PluginLog.info(\n                                plugin.name,\n                                \"RemoteWeb.Cache.FetchError\",\n                                { url, error: err },\n                            );\n                            resolve(\n                                new Response(\n                                    \"Fetch error: \" + url + \", \" + err.message,\n                                    {\n                                        status: 500,\n                                        headers: {\n                                            \"content-type\": \"text/plain\",\n                                        },\n                                    },\n                                ),\n                            );\n                        });\n                });\n            };\n            webSession.protocol.handle(\"https\", requestHandler);\n            webSession.protocol.handle(\"http\", requestHandler);\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/misc/index.ts",
    "content": "import archiver from \"archiver\";\nimport axios from \"axios\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport yauzl from \"yauzl\";\n\nconst getZipFileContent = async (path: string, pathInZip: string) => {\n    return new Promise((resolve, reject) => {\n        // console.log('getZipFileContent', path, pathInZip)\n        yauzl.open(path, { lazyEntries: true }, (err: any, zipfile: any) => {\n            if (err) {\n                // console.log('getZipFileContent err', err)\n                reject(err);\n                return;\n            }\n            zipfile.on(\"error\", function (err: any) {\n                // console.log('getZipFileContent error', err)\n                reject(err);\n            });\n            zipfile.on(\"end\", function () {\n                // console.log('getZipFileContent end')\n                reject(\"FileNotFound\");\n            });\n            zipfile.on(\"entry\", function (entry: any) {\n                // console.log('getZipFileContent entry', entry.fileName)\n                if (entry.fileName === pathInZip) {\n                    zipfile.openReadStream(\n                        entry,\n                        function (err: any, readStream: any) {\n                            if (err) {\n                                reject(err);\n                                return;\n                            }\n                            let chunks: any[] = [];\n                            readStream.on(\"data\", function (chunk: any) {\n                                chunks.push(chunk);\n                            });\n                            readStream.on(\"end\", function () {\n                                const bytes = Buffer.concat(chunks);\n                                const text = bytes.toString(\"utf8\");\n                                resolve(text);\n                            });\n                        },\n                    );\n                } else {\n                    zipfile.readEntry();\n                }\n            });\n            zipfile.readEntry();\n        });\n    });\n};\n\nconst unzip = async (\n    zipPath: string,\n    dest: string,\n    option?: {\n        process: (type: \"start\" | \"end\", entry: any) => void;\n    },\n) => {\n    option = Object.assign(\n        {\n            process: null,\n        },\n        option,\n    );\n    if (!fs.existsSync(dest)) {\n        fs.mkdirSync(dest, { recursive: true });\n    }\n    return new Promise((resolve, reject) => {\n        // console.log('unzip', zipPath, dest)\n        yauzl.open(zipPath, { lazyEntries: true }, (err: any, zipfile: any) => {\n            if (err) {\n                // console.log('unzip err', err)\n                reject(err);\n                return;\n            }\n            zipfile.on(\"error\", function (err: any) {\n                // console.log('unzip error', err)\n                reject(err);\n            });\n            zipfile.on(\"end\", function () {\n                // console.log('unzip end')\n                resolve(undefined);\n            });\n            zipfile.on(\"entry\", function (entry: any) {\n                if (option.process) {\n                    option.process(\"start\", entry);\n                }\n                // console.log('unzip entry', dest, entry.fileName)\n                const destPath = dest + \"/\" + entry.fileName;\n                if (/\\/$/.test(entry.fileName)) {\n                    // console.log('unzip mkdir', destPath)\n                    fs.mkdirSync(destPath, { recursive: true });\n                    zipfile.readEntry();\n                } else {\n                    const dirname = destPath.replace(/\\/[^/]+$/, \"\");\n                    if (!fs.existsSync(dirname)) {\n                        fs.mkdirSync(dirname, { recursive: true });\n                    }\n                    zipfile.openReadStream(\n                        entry,\n                        function (err: any, readStream: any) {\n                            if (err) {\n                                reject(err);\n                                return;\n                            }\n                            readStream.on(\"end\", function () {\n                                if (option.process) {\n                                    option.process(\"end\", entry);\n                                }\n                                zipfile.readEntry();\n                            });\n                            readStream.pipe(fs.createWriteStream(destPath));\n                        },\n                    );\n                }\n            });\n            zipfile.readEntry();\n        });\n    });\n};\n\nconst zip = async (\n    zipPath: string,\n    sourceDir: string,\n    option?: {\n        end?: (archive: any) => Promise<void>;\n        filter?: (params: {\n            name: string;\n            path: string;\n            fullPath: string;\n            isDir: boolean;\n        }) => Promise<boolean>;\n    },\n): Promise<void> => {\n    option = Object.assign(\n        {\n            end: null,\n            filter: null,\n        },\n        option,\n    );\n    return new Promise((resolve, reject) => {\n        const output = fs.createWriteStream(zipPath);\n        const archive = archiver(\"zip\", {\n            zlib: { level: 9 },\n        });\n        output.on(\"close\", function () {\n            resolve(undefined);\n        });\n        archive.on(\"error\", function (err: any) {\n            reject(err);\n        });\n        archive.pipe(output);\n\n        const addFiles = async (dir: string, relativePath: string = \"\") => {\n            const items = fs.readdirSync(path.join(dir, relativePath));\n            for (const item of items) {\n                const fullPath = path.join(dir, relativePath, item);\n                const relPath = path\n                    .join(relativePath, item)\n                    .replace(/\\\\/g, \"/\"); // Normalize for zip\n                const stat = fs.statSync(fullPath);\n                const isDir = stat.isDirectory();\n                const shouldInclude =\n                    !option.filter ||\n                    (await option.filter({\n                        name: item,\n                        path: relPath,\n                        fullPath,\n                        isDir,\n                    }));\n                if (isDir) {\n                    if (shouldInclude) {\n                        await addFiles(dir, relPath);\n                    }\n                } else {\n                    if (shouldInclude) {\n                        archive.file(fullPath, { name: relPath });\n                    }\n                }\n            }\n        };\n\n        addFiles(sourceDir)\n            .then(async () => {\n                if (option.end) {\n                    await option.end(archive);\n                }\n                archive.finalize();\n            })\n            .catch(reject);\n    });\n};\n\nconst request = async (option: {\n    url: string;\n    method?: \"GET\" | \"POST\";\n    responseType?: \"json\" | \"text\" | \"arraybuffer\";\n    headers?: any;\n    data?: any;\n}) => {\n    option = Object.assign(\n        {\n            url: \"\",\n            method: \"GET\",\n            responseType: \"json\",\n            headers: {},\n            data: null,\n        },\n        option,\n    );\n    const response = await axios.request({\n        url: option.url,\n        method: option.method,\n        responseType:\n            option.responseType === \"arraybuffer\" ? \"arraybuffer\" : \"text\",\n        headers: option.headers,\n        data: option.data,\n    });\n    if (response.status !== 200) {\n        throw new Error(`Request failed with status code ${response.status}`);\n    }\n    if (option.responseType === \"json\") {\n        return JSON.parse(response.data);\n    } else if (option.responseType === \"text\") {\n        return response.data;\n    } else if (option.responseType === \"arraybuffer\") {\n        return Buffer.from(response.data);\n    } else {\n        return response.data;\n    }\n};\n\nconst getNetworkInterfaces = () => {\n    const interfaces = os.networkInterfaces();\n    const result: Array<{\n        name: string;\n        address: string;\n        family: string;\n        internal: boolean;\n    }> = [];\n\n    for (const [name, addresses] of Object.entries(interfaces)) {\n        if (!addresses) continue;\n\n        for (const addr of addresses) {\n            // Filter out internal (loopback) addresses and only include IPv4\n            if (!addr.internal && addr.family === \"IPv4\") {\n                result.push({\n                    name,\n                    address: addr.address,\n                    family: addr.family,\n                    internal: addr.internal,\n                });\n            }\n        }\n    }\n\n    return result;\n};\n\nexport const Misc = {\n    getZipFileContent,\n    unzip,\n    zip,\n    request,\n    getNetworkInterfaces,\n};\n\nexport default Misc;\n"
  },
  {
    "path": "electron/mapi/misc/main.ts",
    "content": "import { ipcMain } from \"electron\";\n\nimport index from \"./index\";\n\nipcMain.handle(\n    \"misc:getZipFileContent\",\n    async (_, path: string, pathInZip: string) => {\n        return await index.getZipFileContent(path, pathInZip);\n    },\n);\nipcMain.handle(\"misc:unzip\", async (_, zipPath: string, dest: string) => {\n    return await index.unzip(zipPath, dest);\n});\n\nexport default {\n    ...index,\n};\n\nexport const MiscMain = {\n    ...index,\n};\n"
  },
  {
    "path": "electron/mapi/misc/render.ts",
    "content": "import index from \"./index\";\n\nexport default {\n    ...index,\n};\n"
  },
  {
    "path": "electron/mapi/protocol/main.ts",
    "content": "import { Log } from \"../log/main\";\n\nexport const ProtocolMain = {\n    isReady: false,\n    ready() {\n        this.isReady = true;\n    },\n    url: null,\n    async queue(url: string) {\n        this.url = url;\n        await this.runProtocol();\n    },\n    async runProtocol() {\n        return new Promise<any>(async (resolve) => {\n            const run = async () => {\n                if (!this.isReady) {\n                    setTimeout(run, 100);\n                    return;\n                }\n                if (!this.url) {\n                    Log.info(\n                        \"ProtocolMain.runProtocol.url.Empty\",\n                        this.filePath,\n                    );\n                    return;\n                }\n                const url = this.url;\n                const urlInfo = new URL(url);\n                const command = urlInfo.hostname;\n                const param = urlInfo.searchParams;\n                Log.info(\"ProtocolMain.runProtocol\", {\n                    command,\n                    param,\n                    url,\n                    urlInfo,\n                });\n                if (!command) {\n                    Log.info(\"ProtocolMain.runProtocol.command.Empty\", url);\n                    return;\n                }\n                if (!this.commandListeners[command]) {\n                    Log.info(\n                        \"ProtocolMain.runProtocol.command.NotFound\",\n                        command,\n                    );\n                    return;\n                }\n                for (const callback of this.commandListeners[command]) {\n                    callback(Object.fromEntries(param.entries()));\n                }\n                resolve(undefined);\n            };\n            run().then();\n        });\n    },\n    commandListeners: {} as {\n        [command: string]: Array<(params: { [key: string]: string }) => void>;\n    },\n    register(\n        command: string,\n        callback: (params: { [key: string]: string }) => void,\n    ) {\n        if (!this.commandListeners[command]) {\n            this.commandListeners[command] = [];\n        }\n        this.commandListeners[command].push(callback);\n    },\n    unregister(\n        command: string,\n        callback: (params: { [key: string]: string }) => void,\n    ) {\n        if (!this.commandListeners[command]) {\n            return;\n        }\n        const index = this.commandListeners[command].indexOf(callback);\n        if (index >= 0) {\n            this.commandListeners[command].splice(index, 1);\n        }\n    },\n};\n\nexport default ProtocolMain;\n"
  },
  {
    "path": "electron/mapi/render.ts",
    "content": "import { exposeContext } from \"./util\";\nimport { AppEnv } from \"./env\";\n\nimport config from \"./config/render\";\nimport log from \"./log/render\";\nimport app from \"./app/render\";\nimport storage from \"./storage/render\";\nimport db from \"./db/render\";\nimport file from \"./file/render\";\nimport event from \"./event/render\";\nimport ui from \"./ui/render\";\nimport updater from \"./updater/render\";\nimport statistics from \"./statistics/render\";\nimport user from \"./user/render\";\nimport misc from \"./misc/render\";\nimport manager from \"./manager/render\";\nimport kvdb from \"./kvdb/render\";\n\nexport const MAPI = {\n    init(env: typeof AppEnv = null) {\n        if (!env) {\n            // expose context\n            exposeContext(\"$mapi\", {\n                app,\n                log,\n                config,\n                storage,\n                db,\n                file,\n                event,\n                ui,\n                updater,\n                statistics,\n                user,\n                misc,\n                manager,\n                kvdb,\n            });\n            db.init();\n            event.init();\n            ui.init();\n        } else {\n            // init context\n            AppEnv.appRoot = env.appRoot;\n            AppEnv.appData = env.appData;\n            AppEnv.userData = env.userData;\n            AppEnv.dataRoot = env.dataRoot;\n            AppEnv.isInit = true;\n        }\n    },\n};\n"
  },
  {
    "path": "electron/mapi/statistics/render.ts",
    "content": "import { AppConfig } from \"../../../src/config\";\nimport {\n    memoryInfo,\n    platformArch,\n    platformName,\n    platformUUID,\n    platformVersion,\n} from \"../../lib/env\";\nimport { post } from \"../../lib/api\";\n\nlet tickDataList = [];\n\nlet tickSendTimer = null;\n\nconst tickSendAsync = () => {\n    if (tickSendTimer) {\n        clearTimeout(tickSendTimer);\n        tickSendTimer = null;\n    }\n    if (!AppConfig.statisticsUrl) {\n        tickDataList = [];\n        return;\n    }\n    tickSendTimer = setTimeout(async () => {\n        tickSendTimer = null;\n        if (!tickDataList.length) {\n            return;\n        }\n        // console.log('tickSend', JSON.stringify(tickDataList))\n        post(AppConfig.statisticsUrl, {\n            data: tickDataList,\n            version: AppConfig.version,\n            uuid: platformUUID(),\n            platform: {\n                name: platformName(),\n                version: platformVersion(),\n                arch: platformArch(),\n                mem: memoryInfo(),\n            },\n        })\n            .then((res) => {\n                // console.log('tickSend', tickDataList, res)\n            })\n            .catch((err) => {\n                // console.error('tickSend', tickDataList, err)\n            });\n        tickDataList = [];\n    }, 2000);\n};\n\nconst tick = (name: string, data: any) => {\n    tickDataList.push({\n        name,\n        data,\n    });\n    tickSendAsync();\n};\n\nexport default {\n    tick,\n};\n"
  },
  {
    "path": "electron/mapi/storage/main.ts",
    "content": "import { AppEnv, waitAppEnvReady } from \"../env\";\nimport fs from \"node:fs\";\nimport { ipcMain } from \"electron\";\nimport nodePath from \"node:path\";\n\nlet data = {};\n\nconst userDataRoot = () => {\n    return nodePath.join(AppEnv.userData, \"storage\");\n};\n\nconst dataRoot = () => {\n    return nodePath.join(AppEnv.dataRoot, \"storage\");\n};\n\nconst filePath = (group: string) => {\n    let p = nodePath.join(userDataRoot(), `${group}.json`);\n    if (fs.existsSync(p)) {\n        return p;\n    }\n    return nodePath.join(dataRoot(), `${group}.json`);\n};\n\nconst load = (group: string) => {\n    try {\n        const p = filePath(group);\n        let json = fs.readFileSync(p).toString();\n        json = JSON.parse(json);\n        data[group] = json || {};\n    } catch (e) {\n        data[group] = {};\n    }\n};\n\nconst loadIfNeed = (group: string) => {\n    if (!(group in data)) {\n        load(group);\n    }\n};\n\nconst save = (group: string) => {\n    const path = filePath(group);\n    const dir = nodePath.dirname(path);\n    if (!fs.existsSync(dir)) {\n        fs.mkdirSync(dir, { recursive: true });\n    }\n    fs.writeFileSync(path, JSON.stringify(data[group], null, 4));\n};\n\nconst all = async (group: string) => {\n    await waitAppEnvReady();\n    loadIfNeed(group);\n    return data[group];\n};\n\nconst get = async (group: string, key: string, defaultValue: any) => {\n    await waitAppEnvReady();\n    loadIfNeed(group);\n    if (!(key in data[group])) {\n        data[group][key] = defaultValue;\n        save(group);\n    }\n    return data[group][key];\n};\n\nconst set = async (group: string, key: string, value: any) => {\n    await waitAppEnvReady();\n    loadIfNeed(group);\n    data[group][key] = value;\n    save(group);\n};\n\nconst read = async (group: string, defaultValue: any) => {\n    await waitAppEnvReady();\n    loadIfNeed(group);\n    if (!(group in data)) {\n        data[group] = defaultValue;\n        save(group);\n    }\n    return data[group];\n};\n\nconst write = async (group: string, value: any) => {\n    await waitAppEnvReady();\n    loadIfNeed(group);\n    data[group] = value;\n    save(group);\n};\n\nipcMain.handle(\"storage:all\", async (event, group: string) => {\n    return await all(group);\n});\n\nipcMain.handle(\n    \"storage:get\",\n    async (event, group: string, key: string, defaultValue: any) => {\n        return await get(group, key, defaultValue);\n    },\n);\n\nipcMain.handle(\n    \"storage:set\",\n    async (event, group: string, key: string, value: any) => {\n        return await set(group, key, value);\n    },\n);\n\nipcMain.handle(\n    \"storage:read\",\n    async (event, group: string, defaultValue: any) => {\n        return await read(group, defaultValue);\n    },\n);\n\nipcMain.handle(\"storage:write\", async (event, group: string, value: any) => {\n    return await write(group, value);\n});\n\nexport const StorageMain = {\n    all,\n    get,\n    set,\n    read,\n    write,\n};\n\nexport default StorageMain;\n"
  },
  {
    "path": "electron/mapi/storage/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\n\nconst all = async (group: string) => {\n    return ipcRenderer.invoke(\"storage:all\", group);\n};\n\nconst get = async (group: string, key: string, defaultValue: any) => {\n    return ipcRenderer.invoke(\n        \"storage:get\",\n        group,\n        key,\n        JSON.parse(JSON.stringify(defaultValue)),\n    );\n};\n\nconst set = async (group: string, key: string, value: any) => {\n    return ipcRenderer.invoke(\n        \"storage:set\",\n        group,\n        key,\n        JSON.parse(JSON.stringify(value)),\n    );\n};\n\nconst read = async (group: string, defaultValue: any = null) => {\n    return ipcRenderer.invoke(\n        \"storage:read\",\n        group,\n        JSON.parse(JSON.stringify(defaultValue)),\n    );\n};\n\nconst write = async (group: string, value: any) => {\n    return ipcRenderer.invoke(\n        \"storage:write\",\n        group,\n        JSON.parse(JSON.stringify(value)),\n    );\n};\n\nexport default {\n    all,\n    get,\n    set,\n    read,\n    write,\n};\n"
  },
  {
    "path": "electron/mapi/ui/index.ts",
    "content": "export default {};\n"
  },
  {
    "path": "electron/mapi/ui/render.ts",
    "content": "const init = () => {\n    // initLoaders()\n};\n\nconst initLoaders = () => {\n    function domReady(\n        condition: DocumentReadyState[] = [\"complete\", \"interactive\"],\n    ) {\n        return new Promise((resolve) => {\n            if (condition.includes(document.readyState)) {\n                resolve(true);\n            } else {\n                document.addEventListener(\"readystatechange\", () => {\n                    if (condition.includes(document.readyState)) {\n                        resolve(true);\n                    }\n                });\n            }\n        });\n    }\n\n    const safeDOM = {\n        append(parent: HTMLElement, child: HTMLElement) {\n            if (!Array.from(parent.children).find((e) => e === child)) {\n                return parent.appendChild(child);\n            }\n        },\n        remove(parent: HTMLElement, child: HTMLElement) {\n            if (Array.from(parent.children).find((e) => e === child)) {\n                return parent.removeChild(child);\n            }\n        },\n    };\n\n    /**\n     * https://tobiasahlin.com/spinkit\n     * https://connoratherton.com/loaders\n     * https://projects.lukehaas.me/css-loaders\n     * https://matejkustec.github.io/SpinThatShit\n     */\n    function useLoading() {\n        const className = `loaders-css__square-spin`;\n        const styleContent = `\n@keyframes loading-spin {\n    33%{background-size:calc(100%/3) 0%  ,calc(100%/3) 100%,calc(100%/3) 100%}\n    50%{background-size:calc(100%/3) 100%,calc(100%/3) 0%  ,calc(100%/3) 100%}\n    66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0%  }\n}\n.${className} > div {\n  width: 60px;\n  aspect-ratio: 4;\n  --_g: no-repeat radial-gradient(circle closest-side,#cbd5e1 90%,#cbd5e100);\n  background:\n    var(--_g) 0%   50%,\n    var(--_g) 50%  50%,\n    var(--_g) 100% 50%;\n  background-size: calc(100%/3) 100%;\n  animation: loading-spin 1s infinite linear;\n}\n.app-loading-wrap {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100vw;\n  height: 100vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background: #FFFFFF;\n  z-index: 10000;\n}\n[data-theme=\"dark\"] .app-loading-wrap {\n    background: #17171A;\n}\n[data-theme=\"dark\"] .${className} > div {\n    --_g: no-repeat radial-gradient(circle closest-side,#2D3748 90%,#2D374800);\n}\n    `;\n        const oStyle = document.createElement(\"style\");\n        const oDiv = document.createElement(\"div\");\n        let hasLoading = false;\n        let setLoadingTimer = null;\n\n        oStyle.id = \"app-loading-style\";\n        oStyle.innerHTML = styleContent;\n        oDiv.className = \"app-loading-wrap\";\n        oDiv.innerHTML = `<div class=\"${className}\"><div></div></div>`;\n\n        return {\n            appendLoading() {\n                setLoadingTimer = setTimeout(() => {\n                    safeDOM.append(document.head, oStyle);\n                    safeDOM.append(document.body, oDiv);\n                    hasLoading = true;\n                }, 1000);\n            },\n            removeLoading() {\n                clearTimeout(setLoadingTimer);\n                if (hasLoading) {\n                    safeDOM.remove(document.head, oStyle);\n                    safeDOM.remove(document.body, oDiv);\n                    hasLoading = false;\n                }\n            },\n        };\n    }\n\n    const { appendLoading, removeLoading } = useLoading();\n\n    const isMain = () => {\n        return true;\n        let l = window.location.href;\n        if (l.indexOf(\"app.asar/dist/index.html\") > 0) {\n            return true;\n        }\n        if (l.indexOf(\"localhost\") > 0 && l.indexOf(\".html\") === -1) {\n            return true;\n        }\n        return false;\n    };\n\n    if (isMain()) {\n        domReady().then(appendLoading);\n        window.onmessage = (ev) => {\n            ev.data.payload === \"removeLoading\" && removeLoading();\n        };\n    }\n\n    setTimeout(removeLoading, 4999);\n};\n\nexport default {\n    init,\n};\n"
  },
  {
    "path": "electron/mapi/updater/index.ts",
    "content": "import { AppConfig } from \"../../../src/config\";\nimport {\n    platformArch,\n    platformName,\n    platformUUID,\n    platformVersion,\n} from \"../../lib/env\";\n\nconst checkForUpdate = async () => {\n    try {\n        const res = await fetch(AppConfig.updaterUrl, {\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                version: AppConfig.version,\n                uuid: platformUUID(),\n                platform: {\n                    name: platformName(),\n                    version: platformVersion(),\n                    arch: platformArch(),\n                },\n            }),\n        });\n        return await res.json();\n    } catch (e) {\n        return {\n            code: -1,\n            msg: `Failed to check update : ${e.message}`,\n        };\n    }\n};\n\nexport default {\n    checkForUpdate,\n};\n"
  },
  {
    "path": "electron/mapi/updater/main.ts",
    "content": "import updaterIndex from \"./index\";\nimport { ipcMain } from \"electron\";\nimport ConfigMain from \"../config/main\";\n\nipcMain.handle(\"updater:getCheckAtLaunch\", async (event) => {\n    return ConfigMain.get(\"updaterCheckAtLaunch\", \"yes\");\n});\n\nipcMain.handle(\"updater:setCheckAtLaunch\", async (event, value) => {\n    return ConfigMain.set(\"updaterCheckAtLaunch\", value);\n});\n\nexport const UpdaterMain = {\n    ...updaterIndex,\n};\n\nexport default UpdaterMain;\n"
  },
  {
    "path": "electron/mapi/updater/render.ts",
    "content": "import updaterIndex from \"./index\";\nimport { ipcRenderer } from \"electron\";\n\nconst getCheckAtLaunch = async (): Promise<\"yes\" | \"no\"> => {\n    return ipcRenderer.invoke(\"updater:getCheckAtLaunch\");\n};\n\nconst setCheckAtLaunch = async (value: \"yes\" | \"no\"): Promise<void> => {\n    return ipcRenderer.invoke(\"updater:setCheckAtLaunch\", value);\n};\n\nexport default {\n    ...updaterIndex,\n    getCheckAtLaunch,\n    setCheckAtLaunch,\n};\n"
  },
  {
    "path": "electron/mapi/user/main.ts",
    "content": "import { ipcMain, shell } from \"electron\";\nimport { AppConfig } from \"../../../src/config\";\nimport { ResultType } from \"../../lib/api\";\nimport { Events } from \"../event/main\";\nimport { platformUUID } from \"../../lib/env\";\nimport { AppsMain } from \"../app/main\";\nimport Apps from \"../app\";\nimport StorageMain from \"../storage/main\";\nimport { Log } from \"../log/main\";\nimport { ManagerPluginEvent } from \"../manager/plugin/event\";\n\nconst init = async () => {\n    setTimeout(() => {\n        refresh().then();\n    }, 1000);\n    return null;\n};\n\nconst userData = {\n    isInit: false,\n    apiToken: \"\",\n    user: {\n        id: \"\",\n        name: \"\",\n        avatar: \"\",\n        deviceCode: \"\",\n    },\n    data: {},\n    basic: {},\n};\n\nconst get = async (): Promise<{\n    apiToken: string;\n    user: {\n        id: string;\n        name: string;\n        avatar: string;\n        deviceCode: string;\n    };\n    data: {\n        [key: string]: any;\n    };\n    basic: {\n        [key: string]: any;\n    };\n}> => {\n    if (!userData.isInit) {\n        const userStorageData = await StorageMain.get(\"user\", \"data\", {});\n        userData.apiToken = userStorageData.apiToken || \"\";\n        userData.user = userStorageData.user || {};\n        userData.data = userStorageData.data || {};\n        userData.basic = userStorageData.basic || {};\n        userData.isInit = true;\n    }\n    userData.user.id = userData.user.id || \"\";\n    return {\n        apiToken: userData.apiToken,\n        user: userData.user,\n        data: userData.data,\n        basic: userData.basic,\n    };\n};\n\nipcMain.handle(\n    \"user:open\",\n    async (\n        event,\n        option?: {\n            readyParam: {\n                page?: string;\n                [key: string]: any;\n            };\n        },\n    ) => {\n        option = Object.assign(\n            {\n                readyParam: null,\n            },\n            option || {},\n        );\n        await AppsMain.windowOpen(\"user\", option);\n        if (option.readyParam) {\n            await Events.callPage(\"user\", \"ready\", option.readyParam);\n        }\n    },\n);\n\nipcMain.handle(\"user:get\", async (event) => {\n    return get();\n});\n\nconst save = async (data: {\n    apiToken: string;\n    user: any;\n    data: any;\n    basic: {};\n}) => {\n    const userChanged =\n        JSON.stringify(userData.user) !== JSON.stringify(data.user);\n    userData.apiToken = data.apiToken || \"\";\n    userData.user = data.user || {};\n    userData.data = data.data || {};\n    userData.user.id = userData.user.id || \"\";\n    if (userChanged) {\n        Events.broadcast(\"UserChange\", {});\n        ManagerPluginEvent.firePluginEvent(\"UserChange\", {}).then();\n    }\n    await StorageMain.set(\"user\", \"data\", {\n        apiToken: data.apiToken,\n        user: data.user,\n        data: data.data,\n        basic: data.basic,\n    });\n};\n\nipcMain.handle(\"user:save\", async (event, data) => {\n    return save(data);\n});\n\nconst refresh = async () => {\n    const result = await userInfoApi();\n    // console.log(\"user.refresh\", JSON.stringify(result, null, 2));\n    await save({\n        apiToken: result.data.apiToken,\n        user: result.data.user,\n        data: result.data.data,\n        basic: result.data.basic,\n    });\n};\n\nipcMain.handle(\"user:refresh\", async (event) => {\n    return refresh();\n});\n\nconst getApiToken = async (): Promise<string> => {\n    await get();\n    return userData.apiToken;\n};\n\nipcMain.handle(\"user:getApiToken\", async (event) => {\n    return getApiToken();\n});\n\nconst getWebEnterUrl = async (url: string) => {\n    let param = [];\n    const apiToken = await getApiToken();\n    if (apiToken) {\n        param.push(`api_token=${apiToken}`);\n    }\n    if (await AppsMain.shouldDarkMode()) {\n        param.push(`is_dark=1`);\n    }\n    param.push(`device_uuid=${platformUUID()}`);\n    param.push(`url=${encodeURIComponent(url)}`);\n    return `${AppConfig.apiBaseUrl}/app_manager/enter?${param.join(\"&\")}`;\n};\n\nipcMain.handle(\"user:getWebEnterUrl\", async (event, url) => {\n    return getWebEnterUrl(url);\n});\n\nconst openWebUrl = async (url: string) => {\n    url = await getWebEnterUrl(url);\n    await shell.openExternal(url);\n};\n\nipcMain.handle(\"user:openWebUrl\", async (event, url) => {\n    return openWebUrl(url);\n});\n\nconst apiPost = async (\n    url: string,\n    data: Record<string, any>,\n    option?: {\n        throwException?: boolean;\n    },\n) => {\n    return post(url, data, option);\n};\n\nipcMain.handle(\"user:apiPost\", async (event, url, data, option) => {\n    return apiPost(url, data, option);\n});\n\nexport const User = {\n    init,\n    get,\n    save,\n    getApiToken,\n    getWebEnterUrl,\n    openWebUrl,\n};\n\nexport default User;\n\nconst post = async <T>(\n    api: string,\n    data: Record<string, any>,\n    option?: {\n        throwException?: boolean;\n        retry?: number;\n        retryTimes?: number;\n        retryInterval?: number;\n    },\n): Promise<ResultType<T>> => {\n    option = Object.assign(\n        {\n            throwException: true,\n            retry: 0,\n            retryTimes: 0,\n            retryInterval: 5,\n        },\n        option,\n    );\n    let url = api;\n    if (!api.startsWith(\"http:\") && !api.startsWith(\"https:\")) {\n        url = `${AppConfig.apiBaseUrl}/${api}`;\n    }\n    const apiToken = await User.getApiToken();\n    let json = null,\n        res = null;\n    try {\n        res = await fetch(url, {\n            method: \"POST\",\n            headers: {\n                \"User-Agent\": Apps.getUserAgent(),\n                \"Content-Type\": \"application/json\",\n                \"Api-Token\": apiToken,\n            },\n            body: JSON.stringify(data),\n        });\n        if (res.status !== 200) {\n            if (option.retry > 0 && option.retryTimes < option.retry) {\n                option.retryTimes++;\n                Log.info(\"user.post.retry\", {\n                    api,\n                    data,\n                    res,\n                    retryTimes: option.retryTimes,\n                });\n                await new Promise((resolve) =>\n                    setTimeout(resolve, option.retryInterval * 1000),\n                );\n                return await post(api, data, option);\n            }\n            Log.error(\"user.post.error\", { api, data, res });\n            if (option.throwException) {\n                throw `RequestError(code:${res.status},text:${res.statusText})`;\n            }\n            return {\n                code: 10000,\n                msg: `RequestError(code:${res.status},text:${res.statusText})`,\n            } as ResultType<T>;\n        }\n        json = await res.json();\n    } catch (e) {\n        res = `RequestError(${e})`;\n    }\n    // console.log('post', JSON.stringify({api, data, json}, null, 2))\n    if (!json || !(\"code\" in json)) {\n        if (option.retry > 0 && option.retryTimes < option.retry) {\n            option.retryTimes++;\n            Log.info(\"user.post.retry\", {\n                api,\n                data,\n                res,\n                retryTimes: option.retryTimes,\n            });\n            await new Promise((resolve) =>\n                setTimeout(resolve, option.retryInterval * 1000),\n            );\n            return await post(api, data, option);\n        }\n        Log.error(\"user.post.error\", { api, data, res });\n        if (option.throwException) {\n            throw \"ResponseError\";\n        }\n        return { code: 10000, msg: \"ResponseError\" };\n    }\n    if (json.code) {\n        // login required\n        if (json.code === 1001) {\n            if (userData.user && userData.user.id) {\n                await refresh();\n            }\n        }\n        if (option.throwException) {\n            throw json.msg;\n        }\n    }\n    return json;\n};\n\nconst userInfoApi = async (): Promise<\n    ResultType<{\n        apiToken: string;\n        user: object;\n        data: any;\n        basic: object;\n    }>\n> => {\n    return await post(\"app_manager/user_info\", {});\n};\n\nexport const UserApi = {\n    post,\n    userInfoApi,\n};\n"
  },
  {
    "path": "electron/mapi/user/render.ts",
    "content": "import { ipcRenderer } from \"electron\";\n\nconst open = async (option: any) => {\n    return ipcRenderer.invoke(\"user:open\", option);\n};\n\nconst get = async (): Promise<any> => {\n    return ipcRenderer.invoke(\"user:get\");\n};\n\nconst refresh = async () => {\n    return ipcRenderer.invoke(\"user:refresh\");\n};\n\nconst getApiToken = async (): Promise<string> => {\n    return ipcRenderer.invoke(\"user:getApiToken\");\n};\n\nconst getWebEnterUrl = async (url: string) => {\n    return ipcRenderer.invoke(\"user:getWebEnterUrl\", url);\n};\n\nconst openWebUrl = async (url: string) => {\n    return ipcRenderer.invoke(\"user:openWebUrl\", url);\n};\n\nconst apiPost = async (\n    url: string,\n    data: Record<string, any>,\n    option?: {\n        throwException?: boolean;\n    },\n) => {\n    return ipcRenderer.invoke(\"user:apiPost\", url, data, option);\n};\n\nexport default {\n    open,\n    get,\n    refresh,\n    getApiToken,\n    getWebEnterUrl,\n    openWebUrl,\n    apiPost,\n};\n"
  },
  {
    "path": "electron/mapi/util.ts",
    "content": "import { contextBridge } from \"electron\";\n\nexport function exposeContext(key, value) {\n    if (process.contextIsolated) {\n        try {\n            contextBridge.exposeInMainWorld(key, value);\n        } catch (error) {\n            console.error(error);\n        }\n    } else {\n        window[key] = value;\n    }\n}\n"
  },
  {
    "path": "electron/page/about.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { t } from \"../config/lang\";\nimport { WindowConfig } from \"../config/window\";\nimport { preloadDefault } from \"../lib/env-main\";\nimport { Page } from \"./index\";\n\nexport const PageAbout = {\n    NAME: \"about\",\n    open: async (option: any) => {\n        const win = new BrowserWindow({\n            title: t(\"page.about.title\"),\n            parent: null,\n            minWidth: WindowConfig.aboutWidth,\n            minHeight: WindowConfig.aboutHeight,\n            width: WindowConfig.aboutWidth,\n            height: WindowConfig.aboutHeight,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                webSecurity: false,\n                webviewTag: true,\n                // Consider using contextBridge.exposeInMainWorld\n                // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n                contextIsolation: false,\n            },\n            show: true,\n            frame: false,\n            transparent: false,\n        });\n        return Page.openWindow(PageAbout.NAME, win, \"page/about.html\");\n    },\n};\n"
  },
  {
    "path": "electron/page/feedback.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { t } from \"../config/lang\";\nimport { WindowConfig } from \"../config/window\";\nimport { preloadDefault } from \"../lib/env-main\";\nimport { Page } from \"./index\";\n\nexport const PageFeedback = {\n    NAME: \"feedback\",\n    open: async (option: any) => {\n        const win = new BrowserWindow({\n            title: t(\"page.feedback.title\"),\n            parent: null,\n            minWidth: WindowConfig.feedbackWidth,\n            minHeight: WindowConfig.feedbackHeight,\n            width: WindowConfig.feedbackWidth,\n            height: WindowConfig.feedbackHeight,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                webSecurity: false,\n                webviewTag: true,\n                // Consider using contextBridge.exposeInMainWorld\n                // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n                contextIsolation: false,\n            },\n            show: true,\n            frame: false,\n            transparent: false,\n        });\n        return Page.openWindow(PageFeedback.NAME, win, \"page/feedback.html\");\n    },\n};\n"
  },
  {
    "path": "electron/page/guide.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { preloadDefault, rendererLoadPath } from \"../lib/env-main\";\nimport { Page } from \"./index\";\nimport { AppConfig } from \"../../src/config\";\nimport { icnsLogoPath, icoLogoPath, logoPath } from \"../config/icon\";\nimport { isPackaged } from \"../lib/env\";\nimport { WindowConfig } from \"../config/window\";\nimport * as remoteMain from \"@electron/remote/main\";\nimport { DevToolsManager } from \"../lib/devtools\";\n\nexport const PageGuide = {\n    NAME: \"guide\",\n    open: async (option: any) => {\n        let icon = logoPath;\n        if (process.platform === \"win32\") {\n            icon = icoLogoPath;\n        } else if (process.platform === \"darwin\") {\n            icon = icnsLogoPath;\n        }\n        const win = new BrowserWindow({\n            show: true,\n            title: AppConfig.title,\n            ...(!isPackaged ? { icon } : {}),\n            frame: false,\n            transparent: false,\n            hasShadow: true,\n            center: true,\n            useContentSize: true,\n            minWidth: WindowConfig.guideWidth,\n            minHeight: WindowConfig.guideHeight,\n            width: WindowConfig.guideWidth,\n            height: WindowConfig.guideHeight,\n            skipTaskbar: true,\n            resizable: false,\n            maximizable: false,\n            backgroundColor: \"#f1f5f9\",\n            alwaysOnTop: false,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                webSecurity: false,\n                webviewTag: true,\n                // Consider using contextBridge.exposeInMainWorld\n                // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n                contextIsolation: false,\n            },\n        });\n\n        win.on(\"closed\", () => {\n            Page.unregisterWindow(PageGuide.NAME);\n        });\n\n        rendererLoadPath(win, \"page/guide.html\");\n\n        remoteMain.enable(win.webContents);\n\n        win.webContents.on(\"did-finish-load\", () => {\n            Page.ready(\"guide\");\n            DevToolsManager.autoShow(win);\n        });\n        DevToolsManager.register(\"Guide\", win);\n        // win.webContents.setWindowOpenHandler(({url}) => {\n        //     if (url.startsWith('https:')) shell.openExternal(url)\n        //     return {action: 'deny'}\n        // })\n        Page.registerWindow(PageGuide.NAME, win);\n    },\n};\n"
  },
  {
    "path": "electron/page/index.ts",
    "content": "import { Events } from \"../mapi/event/main\";\nimport { AppEnv, AppRuntime } from \"../mapi/env\";\nimport { PageUser } from \"./user\";\nimport { BrowserWindow, shell } from \"electron\";\nimport { rendererLoadPath } from \"../lib/env-main\";\nimport { PageGuide } from \"./guide\";\nimport { PageSetup } from \"./setup\";\nimport { PageAbout } from \"./about\";\nimport { DevToolsManager } from \"../lib/devtools\";\nimport { PageFeedback } from \"./feedback\";\nimport { PagePayment } from \"./payment\";\nimport { PageMonitor } from \"./monitor\";\nimport { PageLog } from \"./log\";\n\nconst Pages = {\n    user: PageUser,\n    guide: PageGuide,\n    setup: PageSetup,\n    payment: PagePayment,\n    about: PageAbout,\n    feedback: PageFeedback,\n    monitor: PageMonitor,\n    log: PageLog,\n};\n\nexport const Page = {\n    ready(name: string) {\n        Events.send(name, \"APP_READY\", {\n            name,\n            AppEnv,\n        });\n    },\n    openWindow: (name: string, win: BrowserWindow, fileName: string) => {\n        win.webContents.on(\"will-navigate\", (event) => {\n            event.preventDefault();\n        });\n        win.webContents.setWindowOpenHandler(() => {\n            return { action: \"deny\" };\n        });\n        win.webContents.setWindowOpenHandler(({ url }) => {\n            if (url.startsWith(\"https:\") || url.startsWith(\"http:\")) {\n                shell.openExternal(url).then();\n            }\n            return { action: \"deny\" };\n        });\n        win.on(\"close\", () => {\n            delete AppRuntime.windows[name];\n        });\n        const promise = new Promise((resolve, reject) => {\n            win.webContents.on(\"did-finish-load\", () => {\n                win.focus();\n                Page.ready(name);\n                DevToolsManager.autoShow(win);\n                resolve(undefined);\n            });\n        });\n        rendererLoadPath(win, fileName);\n        DevToolsManager.register(`Page.${name}`, win);\n        AppRuntime.windows[name] = win;\n        return promise;\n    },\n    open: async (\n        name: string,\n        option?: {\n            singleton?: boolean;\n            parent?: BrowserWindow;\n            [key: string]: any;\n        },\n    ) => {\n        option = Object.assign(\n            {\n                singleton: true,\n                parent: null,\n            },\n            option,\n        );\n        if (!option.parent) {\n            option.parent = AppRuntime.mainWindow;\n        }\n        if (option.singleton && AppRuntime.windows[name]) {\n            AppRuntime.windows[name].show();\n            AppRuntime.windows[name].focus();\n            AppRuntime.windows[name].setParentWindow(option.parent);\n            return;\n        }\n        return Pages[name].open(option);\n    },\n    registerWindow(name: string, win: BrowserWindow) {\n        AppRuntime.windows[name] = win;\n    },\n    unregisterWindow(name: string) {\n        delete AppRuntime.windows[name];\n    },\n};\n"
  },
  {
    "path": "electron/page/log.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { t } from \"../config/lang\";\nimport { WindowConfig } from \"../config/window\";\nimport { preloadDefault } from \"../lib/env-main\";\nimport { AppRuntime } from \"../mapi/env\";\nimport { Page } from \"./index\";\n\nexport const PageLog = {\n    NAME: \"log\",\n    open: async (option: { log: string }) => {\n        if (AppRuntime.windows[PageLog.NAME]) {\n            AppRuntime.windows[PageLog.NAME].close();\n        }\n        const win = new BrowserWindow({\n            title: t(\"page.log.title\"),\n            parent: null,\n            minWidth: WindowConfig.logWidth,\n            minHeight: WindowConfig.logHeight,\n            width: WindowConfig.logWidth,\n            height: WindowConfig.logHeight,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                webSecurity: false,\n                webviewTag: true,\n                // Consider using contextBridge.exposeInMainWorld\n                // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n                contextIsolation: false,\n            },\n            show: true,\n            frame: false,\n            transparent: false,\n        });\n        await Page.openWindow(PageLog.NAME, win, \"page/log.html\");\n        const logInit = {\n            log: option.log,\n        };\n        win.webContents.executeJavaScript(`\n        const logInit = ()=>{\n            if(!window.__logInit){\n                setTimeout(logInit, 100);\n                return;\n            }\n            window.__logInit(${JSON.stringify(logInit)});\n        };logInit();\n        `);\n    },\n};\n"
  },
  {
    "path": "electron/page/monitor.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { t } from \"../config/lang\";\nimport { preloadDefault } from \"../lib/env-main\";\nimport { Events } from \"../mapi/event/main\";\nimport { Page } from \"./index\";\n\nexport const PageMonitor = {\n    NAME: \"monitor\",\n    open: async (option: {\n        title?: string;\n        width?: number;\n        height?: number;\n        [key: string]: any;\n    }) => {\n        option = Object.assign(\n            {\n                title: t(\"page.monitor.title\"),\n                width: 700,\n                height: 500,\n                url: \"\",\n                script: null,\n                openDevTools: false,\n                broadcastPages: [],\n            },\n            option,\n        );\n        const win = new BrowserWindow({\n            title: option.title,\n            width: option.width,\n            height: option.height,\n            webPreferences: {\n                nodeIntegration: true,\n                contextIsolation: false,\n                webSecurity: false,\n                preload: preloadDefault,\n                webviewTag: true,\n            },\n            show: true,\n            frame: false,\n            center: true,\n            transparent: false,\n            focusable: true,\n            parent: null,\n            alwaysOnTop: false,\n        });\n        const sendMonitorData = async (type: string, data: any) => {\n            return Events.callPage(PageMonitor.NAME, \"MonitorData\", {\n                type,\n                data,\n            });\n        };\n        win.webContents.on(\"did-finish-load\", () => {\n            sendMonitorData(\"SetTitle\", { title: option.title });\n            sendMonitorData(\"LoadUrl\", {\n                url: option.url,\n                script: option.script,\n                openDevTools: option.openDevTools,\n            });\n        });\n        win.webContents.on(\"ipc-message\", (event, channel, ...args) => {\n            if (channel === \"MonitorEvent\") {\n                const { type, data } = args[0];\n                // console.log('MonitorEvent', type, data)\n                if (option.broadcastPages.length > 0) {\n                    Events.broadcast(\n                        \"MonitorEvent\",\n                        { type, data },\n                        {\n                            pages: option.broadcastPages,\n                        },\n                    );\n                }\n            }\n        });\n        await Page.openWindow(PageMonitor.NAME, win, \"page/monitor.html\");\n    },\n};\n"
  },
  {
    "path": "electron/page/payment.ts",
    "content": "import { BrowserWindow, ipcMain } from \"electron\";\nimport { preloadDefault, rendererLoadPath } from \"../lib/env-main\";\nimport { Page } from \"./index\";\nimport { AppConfig } from \"../../src/config\";\nimport { icnsLogoPath, icoLogoPath, logoPath } from \"../config/icon\";\nimport { isPackaged } from \"../lib/env\";\nimport { WindowConfig } from \"../config/window\";\nimport * as remoteMain from \"@electron/remote/main\";\nimport { DevToolsManager } from \"../lib/devtools\";\n\nexport const PagePayment = {\n    NAME: \"payment\",\n    event: {\n        onRefresh: null,\n        onWatch: null,\n        onClose: null,\n    },\n    open: async (option: {\n        onRefresh: () => Promise<{\n            payUrl: string;\n            watchUrl: string;\n            payExpireSeconds: number;\n            body: string;\n        }>;\n        onWatch: () => Promise<{\n            status: \"WaitPay\" | \"Scanned\" | \"Payed\" | \"Expired\" | \"Error\";\n        }>;\n        onClose: () => void;\n        parent?: BrowserWindow;\n    }): Promise<{\n        close: () => void;\n    }> => {\n        PagePayment.event.onRefresh = option.onRefresh;\n        PagePayment.event.onWatch = option.onWatch;\n        PagePayment.event.onClose = option.onClose;\n        let icon = logoPath;\n        if (process.platform === \"win32\") {\n            icon = icoLogoPath;\n        } else if (process.platform === \"darwin\") {\n            icon = icnsLogoPath;\n        }\n        let parent = option.parent || null;\n        let alwaysOnTop = !parent;\n        const win = new BrowserWindow({\n            show: true,\n            title: AppConfig.title,\n            ...(!isPackaged ? { icon } : {}),\n            frame: false,\n            transparent: false,\n            hasShadow: true,\n            center: true,\n            useContentSize: true,\n            minWidth: WindowConfig.paymentWidth,\n            minHeight: WindowConfig.paymentHeight,\n            width: WindowConfig.paymentWidth,\n            height: WindowConfig.paymentHeight,\n            skipTaskbar: true,\n            resizable: false,\n            maximizable: false,\n            backgroundColor: \"#f1f5f9\",\n            focusable: true,\n            parent,\n            alwaysOnTop,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                webSecurity: false,\n                webviewTag: true,\n                // Consider using contextBridge.exposeInMainWorld\n                // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n                contextIsolation: false,\n                // sandbox: false,\n            },\n        });\n\n        win.on(\"closed\", () => {\n            Page.unregisterWindow(PagePayment.NAME);\n            PagePayment.event.onClose();\n        });\n\n        rendererLoadPath(win, \"page/payment.html\");\n\n        remoteMain.enable(win.webContents);\n\n        win.webContents.on(\"did-finish-load\", () => {\n            Page.ready(\"payment\");\n            DevToolsManager.autoShow(win);\n            win.focus();\n        });\n        DevToolsManager.register(\"Payment\", win);\n        // win.webContents.setWindowOpenHandler(({url}) => {\n        //     if (url.startsWith('https:')) shell.openExternal(url)\n        //     return {action: 'deny'}\n        // })\n        Page.registerWindow(PagePayment.NAME, win);\n\n        return {\n            close: () => {\n                win.close();\n            },\n        };\n    },\n};\n\nipcMain.handle(\n    \"Payment.Event\",\n    async (event, type: \"refresh\" | \"watch\", param: any) => {\n        switch (type) {\n            case \"refresh\":\n                return await PagePayment.event.onRefresh();\n            case \"watch\":\n                return await PagePayment.event.onWatch();\n        }\n    },\n);\n"
  },
  {
    "path": "electron/page/setup.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { preloadDefault, rendererLoadPath } from \"../lib/env-main\";\nimport { Page } from \"./index\";\nimport { AppConfig } from \"../../src/config\";\nimport { icnsLogoPath, icoLogoPath, logoPath } from \"../config/icon\";\nimport { isPackaged } from \"../lib/env\";\nimport { WindowConfig } from \"../config/window\";\nimport * as remoteMain from \"@electron/remote/main\";\nimport { DevToolsManager } from \"../lib/devtools\";\n\nexport const PageSetup = {\n    NAME: \"setup\",\n    open: async (option: any) => {\n        let icon = logoPath;\n        if (process.platform === \"win32\") {\n            icon = icoLogoPath;\n        } else if (process.platform === \"darwin\") {\n            icon = icnsLogoPath;\n        }\n        const win = new BrowserWindow({\n            show: true,\n            title: AppConfig.title,\n            ...(!isPackaged ? { icon } : {}),\n            frame: false,\n            transparent: false,\n            hasShadow: true,\n            center: true,\n            useContentSize: true,\n            minWidth: WindowConfig.guideWidth,\n            minHeight: WindowConfig.guideHeight,\n            width: WindowConfig.guideWidth,\n            height: WindowConfig.guideHeight,\n            skipTaskbar: true,\n            resizable: false,\n            maximizable: false,\n            backgroundColor: \"#f1f5f9\",\n            alwaysOnTop: true,\n            webPreferences: {\n                preload: preloadDefault,\n                // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production\n                nodeIntegration: true,\n                webSecurity: false,\n                webviewTag: true,\n                // Consider using contextBridge.exposeInMainWorld\n                // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation\n                contextIsolation: false,\n                // sandbox: false,\n            },\n        });\n\n        win.on(\"closed\", () => {\n            Page.unregisterWindow(PageSetup.NAME);\n        });\n\n        rendererLoadPath(win, \"page/setup.html\");\n\n        remoteMain.enable(win.webContents);\n\n        win.webContents.on(\"did-finish-load\", () => {\n            Page.ready(\"setup\");\n            DevToolsManager.autoShow(win);\n        });\n        DevToolsManager.register(\"Setup\", win);\n        // win.webContents.setWindowOpenHandler(({url}) => {\n        //     if (url.startsWith('https:')) shell.openExternal(url)\n        //     return {action: 'deny'}\n        // })\n        Page.registerWindow(PageSetup.NAME, win);\n    },\n};\n"
  },
  {
    "path": "electron/page/user.ts",
    "content": "import { BrowserWindow } from \"electron\";\nimport { t } from \"../config/lang\";\nimport { preloadDefault } from \"../lib/env-main\";\nimport { Page } from \"./index\";\n\nexport const PageUser = {\n    NAME: \"user\",\n    open: async (option: { parent?: BrowserWindow }) => {\n        option = Object.assign(\n            {\n                parent: null,\n            },\n            option,\n        );\n        let alwaysOnTop = !option.parent;\n        const win = new BrowserWindow({\n            title: t(\"page.user.title\"),\n            minWidth: 700,\n            minHeight: 500,\n            width: 700,\n            height: 500,\n            webPreferences: {\n                nodeIntegration: true,\n                contextIsolation: false,\n                webSecurity: false,\n                preload: preloadDefault,\n                webviewTag: true,\n            },\n            show: true,\n            frame: false,\n            center: true,\n            transparent: false,\n            focusable: true,\n            parent: option.parent,\n            alwaysOnTop,\n        });\n        return Page.openWindow(PageUser.NAME, win, \"page/user.html\");\n    },\n};\n"
  },
  {
    "path": "electron/preload/focusany.ts",
    "content": "import electronRemote from \"@electron/remote\";\nimport { ipcRenderer, shell } from \"electron\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport zhCN from \"../../src/lang/zh-CN.json\";\nimport { isMac } from \"../lib/env\";\nimport {\n    EncodeUtil,\n    FileUtil,\n    HotKeyUtil,\n    StrUtil,\n    TimeUtil,\n} from \"../lib/util\";\nconst t = (key: string): string => (zhCN as any)[key] || key;\n\nconst ipcSendSync = (type: string, data?: any) => {\n    executeHook(\"Log\", `${type}`, data);\n    const result = ipcRenderer.sendSync(\"FocusAny.Plugin\", {\n        type,\n        data,\n    });\n    executeHook(\"Log\", `${type}.result`, result);\n    if (result instanceof Error) throw result;\n    return result;\n};\n\nconst ipcSendAsync = async (type: string, data?: any) => {\n    executeHook(\"Log\", `${type}`, data);\n    const result = await ipcRenderer.invoke(\"FocusAny.Plugin.Async\", {\n        type,\n        data,\n    });\n    executeHook(\"Log\", `${type}.result`, result);\n    if (result instanceof Error) throw result;\n    return result;\n};\n\nconst ipcSend = (type: string, data?: any) => {\n    ipcRenderer.send(\"FocusAny.Plugin\", {\n        type,\n        data,\n    });\n    executeHook(\"Log\", `${type}`, data);\n};\n\nconst ipcSendToHost = (\n    type: string,\n    data?: any,\n    hasResult?: boolean,\n): Promise<any> => {\n    hasResult = hasResult || false;\n    const id = StrUtil.randomString(16);\n    return new Promise((resolve, reject) => {\n        if (hasResult) {\n            const timeoutTimer = setTimeout(() => {\n                executeHook(\"Log\", `${type}.timeout`);\n                ipcRenderer.removeAllListeners(`FocusAny.View.${id}`);\n                reject(new Error(\"timeout\"));\n            }, 60 * 1000);\n            ipcRenderer.once(`FocusAny.View.${id}`, (_event, result) => {\n                executeHook(\"Log\", `${type}.result`, result);\n                clearTimeout(timeoutTimer);\n                resolve(result);\n            });\n        }\n        ipcRenderer.sendToHost(\"FocusAny.View\", {\n            id,\n            type,\n            data,\n        });\n        executeHook(\"Log\", `${type}`, data);\n        if (!hasResult) {\n            resolve(null);\n        }\n    });\n};\n\nconst executeHook = (hook: string, ...data: any[]) => {\n    hook = `on${hook}`;\n    if (FocusAny.hooks[hook]) {\n        FocusAny.hooks[hook](...data);\n    }\n};\n\nexport const FocusAny = {\n    hooks: {} as any,\n    onPluginReady(cb: Function) {\n        FocusAny.hooks.onPluginReady = cb;\n    },\n    onPluginExit(cb: Function) {\n        FocusAny.hooks.onPluginExit = cb;\n    },\n\n    onPluginEvent(event: PluginEvent, callback: (data: any) => void) {\n        if (!(\"onPluginEvent\" in FocusAny.hooks)) {\n            FocusAny.hooks.onPluginEvent = (payload: {\n                event: string;\n                data: any;\n            }) => {\n                const { event, data } = payload;\n                if (event in FocusAny.hooks.onPluginEventCallbacks) {\n                    FocusAny.hooks.onPluginEventCallbacks[event].forEach(\n                        (cb: (data: any) => void) => {\n                            cb(data);\n                        },\n                    );\n                }\n            };\n        }\n        if (!(\"onPluginEventCallbacks\" in FocusAny.hooks)) {\n            FocusAny.hooks.onPluginEventCallbacks = {};\n        }\n        if (!(event in FocusAny.hooks.onPluginEventCallbacks)) {\n            FocusAny.hooks.onPluginEventCallbacks[event] = [];\n        }\n        FocusAny.hooks.onPluginEventCallbacks[event].push(callback);\n        ipcSend(\"registerPluginEvent\", { event });\n    },\n    offPluginEvent(event: PluginEvent, callback: (data: any) => void) {\n        if (!(\"onPluginEventCallbacks\" in FocusAny.hooks)) {\n            FocusAny.hooks.onPluginEventCallbacks = {};\n        }\n        if (!(event in FocusAny.hooks.onPluginEventCallbacks)) {\n            return;\n        }\n        FocusAny.hooks.onPluginEventCallbacks[event] =\n            FocusAny.hooks.onPluginEventCallbacks[event].filter(\n                (c) => c !== callback,\n            );\n        if (FocusAny.hooks.onPluginEventCallbacks[event].length === 0) {\n            delete FocusAny.hooks.onPluginEventCallbacks[event];\n            ipcSend(\"unregisterPluginEvent\", { event });\n        }\n    },\n    offPluginEventAll(event: PluginEvent) {\n        if (!(\"onPluginEventCallbacks\" in FocusAny.hooks)) {\n            FocusAny.hooks.onPluginEventCallbacks = {};\n        }\n        if (!(event in FocusAny.hooks.onPluginEventCallbacks)) {\n            return;\n        }\n        delete FocusAny.hooks.onPluginEventCallbacks[event];\n        ipcSend(\"unregisterPluginEvent\", { event });\n    },\n\n    onMoreMenuClick(callback: (data: { name: string }) => void) {\n        FocusAny.hooks.onMoreMenuClick = callback;\n    },\n\n    registerHotkey(\n        key: string | string[] | HotkeyQuickType | HotkeyType | HotkeyType[],\n        callback: () => void,\n    ) {\n        if (\"save\" === key) {\n            if (isMac) {\n                key = \"Command+S\";\n            } else {\n                key = \"Ctrl+S\";\n            }\n        }\n        const hotkeys = HotKeyUtil.unify(key);\n        if (!(\"hotKeyListeners\" in FocusAny.hooks)) {\n            FocusAny.hooks.hotKeyListeners = [];\n            FocusAny.hooks.onHotkey = (payload: {\n                id: string;\n                hotkey: HotkeyType;\n            }) => {\n                const { id, hotkey } = payload;\n                FocusAny.hooks.hotKeyListeners.forEach(\n                    (listener: {\n                        id: string;\n                        hotkeys: HotkeyType[];\n                        callback: () => void;\n                    }) => {\n                        if (listener.id === id) {\n                            listener.callback();\n                        }\n                    },\n                );\n            };\n        }\n        const id = StrUtil.randomString(16);\n        FocusAny.hooks.hotKeyListeners.push({ id, hotkeys, callback });\n        ipcSend(\"registerHotkey\", { id, hotkeys });\n    },\n    unregisterHotkeyAll() {\n        FocusAny.hooks.hotKeyListeners = [];\n        ipcSend(\"unregisterHotkeyAll\", {});\n    },\n    onLog(cb: Function) {\n        FocusAny.hooks.onLog = cb;\n    },\n    isMacOs() {\n        return os.type() === \"Darwin\";\n    },\n    isWindows() {\n        return os.type() === \"Windows_NT\";\n    },\n    isLinux() {\n        return os.type() === \"Linux\";\n    },\n    getPlatformArch() {\n        return ipcSendSync(\"getPlatformArch\");\n    },\n    isMainWindowShown(): boolean {\n        return ipcSendSync(\"isMainWindowShown\");\n    },\n    hideMainWindow() {\n        ipcSend(\"hideMainWindow\", {});\n    },\n    showMainWindow() {\n        ipcSend(\"showMainWindow\", {});\n    },\n    isFastPanelWindowShown() {\n        return ipcSendSync(\"isFastPanelWindowShown\");\n    },\n    showFastPanelWindow() {\n        ipcSend(\"showFastPanelWindow\", {});\n    },\n    hideFastPanelWindow() {\n        ipcSend(\"hideFastPanelWindow\", {});\n    },\n    showOpenDialog(options: {\n        title?: string;\n        defaultPath?: string;\n        buttonLabel?: string;\n        filters?: { name: string; extensions: string[] }[];\n        properties?: Array<\n            | \"openFile\"\n            | \"openDirectory\"\n            | \"multiSelections\"\n            | \"showHiddenFiles\"\n            | \"createDirectory\"\n            | \"promptToCreate\"\n            | \"noResolveAliases\"\n            | \"treatPackageAsDirectory\"\n            | \"dontAddToRecent\"\n        >;\n        message?: string;\n        securityScopedBookmarks?: boolean;\n    }): string[] | undefined {\n        return ipcSendSync(\"showOpenDialog\", options);\n    },\n    showSaveDialog(options: {\n        title?: string;\n        defaultPath?: string;\n        buttonLabel?: string;\n        filters?: { name: string; extensions: string[] }[];\n        message?: string;\n        nameFieldLabel?: string;\n        showsTagField?: string;\n        properties?: Array<\n            | \"showHiddenFiles\"\n            | \"createDirectory\"\n            | \"treatPackageAsDirectory\"\n            | \"showOverwriteConfirmation\"\n            | \"dontAddToRecent\"\n        >;\n        securityScopedBookmarks?: boolean;\n    }): string | undefined {\n        return ipcSendSync(\"showSaveDialog\", options);\n    },\n    setExpendHeight(height: number) {\n        ipcSend(\"setExpendHeight\", height);\n    },\n    setSubInput(\n        onChange: Function,\n        placeholder: string = \"\",\n        isFocus: boolean = true,\n        isVisible: boolean = true,\n    ) {\n        if (typeof onChange === \"function\") {\n            FocusAny.hooks.onSubInputChange = onChange;\n        }\n        ipcSendSync(\"setSubInput\", {\n            placeholder,\n            isFocus,\n            isVisible,\n        });\n    },\n    removeSubInput() {\n        delete FocusAny.hooks.onSubInputChange;\n        ipcSendSync(\"removeSubInput\");\n    },\n    setSubInputValue(text: string) {\n        ipcSendSync(\"setSubInputValue\", { text });\n    },\n    subInputBlur() {\n        ipcSendSync(\"subInputBlur\");\n    },\n    getPluginRoot() {\n        return ipcSendSync(\"getPluginRoot\");\n    },\n    getPluginConfig() {\n        return ipcSendSync(\"getPluginConfig\");\n    },\n    getPluginInfo() {\n        return ipcSendSync(\"getPluginInfo\");\n    },\n    getPluginEnv(): \"dev\" | \"prod\" {\n        return ipcSendSync(\"getPluginEnv\");\n    },\n    getQuery(requestId: string): SearchQuery {\n        return ipcSendSync(\"getQuery\", { requestId });\n    },\n    getPath(\n        name:\n            | \"home\"\n            | \"appData\"\n            | \"userData\"\n            | \"temp\"\n            | \"exe\"\n            | \"desktop\"\n            | \"documents\"\n            | \"downloads\"\n            | \"music\"\n            | \"pictures\"\n            | \"videos\"\n            | \"logs\",\n    ) {\n        return ipcSendSync(\"getPath\", { name });\n    },\n    showToast(\n        body: string,\n        options?: {\n            duration?: number;\n            status?: \"info\" | \"success\" | \"error\";\n        },\n    ): void {\n        ipcSend(\"showToast\", { body, options });\n    },\n    showNotification(body: string, clickActionName?: string) {\n        ipcSend(\"showNotification\", { body, clickActionName });\n    },\n    showMessageBox(\n        message: string,\n        options: {\n            title?: string;\n            yes?: string;\n            no?: string;\n        },\n    ) {\n        options = options || {};\n        return ipcSendSync(\"showMessageBox\", {\n            message,\n            ...options,\n        });\n    },\n\n    copyImage(image: string) {\n        return ipcSendSync(\"copyImage\", { image });\n    },\n    copyText(text: string) {\n        return ipcSendSync(\"copyText\", { text });\n    },\n    copyFile(file: string | string[]) {\n        return ipcSendSync(\"copyFile\", { file });\n    },\n    getClipboardText() {\n        return ipcSendSync(\"getClipboardText\", {});\n    },\n    getClipboardImage() {\n        return ipcSendSync(\"getClipboardImage\", {});\n    },\n    getClipboardFiles(): {\n        name: string;\n        pathname: string;\n        isDirectory: boolean;\n        size: number;\n        lastModified: number;\n    }[] {\n        return ipcSendSync(\"getClipboardFiles\");\n    },\n    async listClipboardItems(option?: { limit?: number }): Promise<\n        {\n            type: \"file\" | \"image\" | \"text\";\n            timestamp: number;\n            files?: FileItem[];\n            image?: string;\n            text?: string;\n        }[]\n    > {\n        return ipcSendAsync(\"listClipboardItems\", { option });\n    },\n    async deleteClipboardItem(timestamp: number): Promise<void> {\n        return ipcSendAsync(\"deleteClipboardItem\", { timestamp });\n    },\n    async clearClipboardItems(): Promise<void> {\n        return ipcSendAsync(\"clearClipboardItems\");\n    },\n    shellOpenExternal(url: string) {\n        shell.openExternal(url).then();\n    },\n    shellOpenPath(path: string) {\n        shell.openPath(path).then();\n    },\n    shellShowItemInFolder(path: string) {\n        ipcSend(\"shellShowItemInFolder\", { path });\n    },\n    shellBeep() {\n        ipcSend(\"shellBeep\");\n    },\n    getFileIcon(path: string) {\n        return ipcSendSync(\"getFileIcon\", { path });\n    },\n\n    simulate: {\n        keyboardTap(\n            key: string,\n            modifiers: (\"ctrl\" | \"shift\" | \"command\" | \"option\" | \"alt\")[],\n        ) {\n            ipcSend(\"simulateKeyboardTap\", { key, modifiers });\n        },\n        typeString(text: string) {\n            ipcSend(\"simulateTypeString\", { text });\n        },\n        mouseToggle(type: \"down\" | \"up\", button: \"left\" | \"right\" | \"middle\") {\n            ipcSend(\"simulateMouseToggle\", { type, button });\n        },\n        mouseMove(x: number, y: number) {\n            ipcSend(\"simulateMouseMove\", { x, y });\n        },\n        mouseClick(button: \"left\" | \"right\" | \"middle\", double?: boolean) {\n            ipcSend(\"simulateMouseClick\", { button, double });\n        },\n    },\n\n    getCursorScreenPoint() {\n        return electronRemote.screen.getCursorScreenPoint();\n    },\n    getDisplayNearestPoint(point: { x: number; y: number }) {\n        return electronRemote.screen.getDisplayNearestPoint(point);\n    },\n    createBrowserWindow(\n        url: string,\n        options: BrowserWindow.InitOptions,\n        callback?: () => void,\n    ) {\n        // console.log('createBrowserWindow', JSON.stringify(url))\n        const pluginRoot = this.getPluginRoot();\n        // console.log('createBrowserWindow', JSON.stringify(url))\n        let preloadPath = null;\n        options = (options || {}) as BrowserWindow.InitOptions;\n        if (options.webPreferences && options.webPreferences.preload) {\n            preloadPath = path.join(pluginRoot, options.webPreferences.preload);\n        }\n        let win = new electronRemote.BrowserWindow({\n            useContentSize: true,\n            resizable: true,\n            title: options.title || t(\"plugin.newWindow\"),\n            show: true,\n            backgroundColor: \"#fff\",\n            ...options,\n            webPreferences: {\n                webSecurity: false,\n                backgroundThrottling: false,\n                contextIsolation: false,\n                webviewTag: true,\n                nodeIntegration: true,\n                spellcheck: false,\n                partition: null,\n                ...(options.webPreferences || {}),\n                preload: preloadPath,\n            },\n        });\n        if (\n            url.startsWith(\"file://\") ||\n            url.startsWith(\"http://\") ||\n            url.startsWith(\"https://\")\n        ) {\n            win.loadURL(url);\n        } else {\n            win.loadFile(url);\n        }\n        win.on(\"closed\", () => {\n            win = undefined;\n        });\n        win.once(\"ready-to-show\", () => {\n            win.show();\n        });\n        win.webContents.on(\"dom-ready\", () => {\n            callback && callback();\n        });\n        return win;\n    },\n    screenCapture(cb: (imgBase64: string) => void): void {\n        FocusAny.hooks.onScreenCapture = (data: { image: string }) => {\n            // console.log('onScreenCapture', data)\n            cb && cb(data.image);\n        };\n        ipcSendSync(\"screenCapture\");\n    },\n    getNativeId(): string {\n        return ipcSendSync(\"getNativeId\");\n    },\n    getAppVersion(): string {\n        return ipcSendSync(\"getAppVersion\");\n    },\n    outPlugin() {\n        ipcSend(\"outPlugin\");\n    },\n    isDarkColors() {\n        return ipcSendSync(\"isDarkColors\");\n    },\n    redirect(keywordsOrAction: string | string[], query?: SearchQuery): void {\n        ipcSend(\"redirect\", { keywordsOrAction, query });\n    },\n    getActions(names?: string[]): PluginAction[] {\n        return ipcSendSync(\"getActions\", { names });\n    },\n    setAction(action: PluginAction | PluginAction[]) {\n        ipcSendSync(\"setAction\", { action });\n    },\n    removeAction(name: string) {\n        ipcSendSync(\"removeAction\", { name });\n    },\n\n    sendBackendEvent(\n        event: string,\n        data?: any,\n        option?: {\n            timeout: number;\n        },\n    ): Promise<any> {\n        option = Object.assign({\n            timeout: 10 * 1000,\n        });\n        return new Promise((resolve, reject) => {\n            const id = StrUtil.randomString(16);\n            const timeoutTimer = setTimeout(() => {\n                ipcRenderer.removeAllListeners(`FocusAny.Event.${id}`);\n                reject(new Error(\"timeout\"));\n            }, option.timeout);\n            ipcRenderer.once(`FocusAny.Event.${id}`, (_event, result) => {\n                clearTimeout(timeoutTimer);\n                resolve(result);\n            });\n            ipcRenderer.send(\"FocusAny.Event\", {\n                id,\n                event,\n                data,\n            });\n            setTimeout(() => {\n                resolve(null);\n            }, 1000);\n        });\n    },\n\n    registerCallPage(\n        type: string,\n        callback: (\n            resolve: (data: any) => void,\n            reject: (error: string) => void,\n            data: any,\n        ) => void,\n        option?: {\n            timeout?: number;\n        },\n    ) {\n        option = Object.assign(\n            {\n                timeout: 30 * 1000,\n            },\n            option || {},\n        );\n        if (!(\"__page\" in window)) {\n            (window as any)[\"__page\"] = {};\n        }\n        if (!(\"callPage\" in (window as any)[\"__page\"])) {\n            (window as any)[\"__page\"].callPage = {};\n        }\n        (window as any)[\"__page\"].callPage[type] = callback;\n    },\n\n    callPage(\n        type: string,\n        data?: any,\n        option?: {\n            timeout?: number;\n        },\n    ): Promise<any> {\n        throw new Error(\"Only can be called in backend.cjs\");\n    },\n\n    setRemoteWebRuntime(info: {\n        userAgent: string;\n        urlMap: Record<string, string>;\n        types: string[];\n        domains: string[];\n        blocks: string[];\n    }): Promise<undefined> {\n        return ipcSendAsync(\"setRemoteWebRuntime\", { info });\n    },\n\n    llmListModels(): Promise<\n        {\n            providerId: string;\n            providerLogo: string;\n            providerTitle: string;\n            modelId: string;\n            modelName: string;\n        }[]\n    > {\n        return ipcSendAsync(\"llmListModels\");\n    },\n\n    llmChat(callInfo: {\n        providerId: string;\n        modelId: string;\n        message: string;\n    }): Promise<{\n        code: number;\n        msg: string;\n        data?: {\n            message: string;\n        };\n    }> {\n        return ipcSendAsync(\"llmChat\", { callInfo });\n    },\n\n    logInfo(label: string, data?: any): void {\n        ipcSend(\"logInfo\", { label, logData: data });\n    },\n\n    logError(label: string, data?: any): void {\n        ipcSend(\"logError\", { label, logData: data });\n    },\n\n    logPath(): Promise<string> {\n        return ipcSendSync(\"logPath\");\n    },\n\n    logShow(): void {\n        ipcSend(\"logShow\");\n    },\n\n    async addLaunch(\n        keyword: string,\n        name: string,\n        hotkey: HotkeyType,\n    ): Promise<void> {\n        return ipcSendAsync(\"addLaunch\", { keyword, name, hotkey });\n    },\n\n    async removeLaunch(keyword: string): Promise<void> {\n        return ipcSendAsync(\"removeLaunch\", { keyword });\n    },\n\n    async activateLatestWindow(): Promise<void> {\n        return ipcSendAsync(\"activateLatestWindow\");\n    },\n\n    showUserLogin() {\n        ipcSend(\"showUserLogin\");\n    },\n\n    getUser() {\n        return ipcSendSync(\"getUser\");\n    },\n\n    getUserAccessToken(): Promise<{ token: string; expireAt: number }> {\n        return ipcSendAsync(\"getUserAccessToken\");\n    },\n\n    listGoods(query?: { ids?: string[] }): Promise<\n        {\n            id: string;\n            title: string;\n            cover: string;\n            priceType: \"fixed\" | \"dynamic\";\n            fixedPrice: string;\n            description: string;\n        }[]\n    > {\n        return ipcSendAsync(\"listGoods\", { query });\n    },\n\n    openGoodsPayment(options: {\n        goodsId: string;\n        price?: string;\n        outOrderId?: string;\n        outParam?: string;\n    }): Promise<{\n        paySuccess: boolean;\n    }> {\n        return ipcSendAsync(\"openGoodsPayment\", { options });\n    },\n\n    queryGoodsOrders(options: {\n        goodsId?: string;\n        page?: number;\n        pageSize?: number;\n    }): Promise<{\n        page: number;\n        total: number;\n        records: {\n            id: string;\n            goodsId: string;\n            status: \"Paid\" | \"Unpaid\";\n        }[];\n    }> {\n        return ipcSendAsync(\"queryGoodsOrders\", { options });\n    },\n\n    apiPost(\n        url: string,\n        body: any,\n        option: {},\n    ): Promise<{\n        code: number;\n        msg: string;\n        data: any;\n    }> {\n        return ipcSendAsync(\"apiPost\", { url, body, option });\n    },\n\n    file: {\n        exists(path: string): Promise<boolean> {\n            return ipcSendAsync(\"fileExists\", { path });\n        },\n        read(\n            path: string,\n            format?: \"string\" | \"buffer\" | \"base64\",\n        ): Promise<string | Uint8Array> {\n            return ipcSendAsync(\"fileRead\", { path, format });\n        },\n        write(\n            path: string,\n            data: string | Uint8Array,\n            option?: {\n                isBase64?: boolean;\n            },\n        ): Promise<void> {\n            return ipcSendAsync(\"fileWrite\", { path, data, option });\n        },\n        remove(path: string): Promise<void> {\n            return ipcSendAsync(\"fileRemove\", { path });\n        },\n        ext(path: string): Promise<string> {\n            return ipcSendAsync(\"fileExt\", { path });\n        },\n        writeTemp(\n            ext: string,\n            data: string | Uint8Array,\n            option?: {\n                isBase64?: boolean;\n            },\n        ): Promise<string> {\n            return ipcSendAsync(\"fileWriteTemp\", { ext, data, option });\n        },\n    },\n\n    db: {\n        put(doc: DbDoc) {\n            return ipcSendSync(\"dbPut\", { doc });\n        },\n        get<T extends {} = Record<string, any>>(id: string): DbDoc<T> | null {\n            return ipcSendSync(\"dbGet\", { id });\n        },\n        remove(doc: string | DbDoc): DbReturn {\n            return ipcSendSync(\"dbRemove\", { doc });\n        },\n        bulkDocs(docs: DbDoc[]): DbReturn[] {\n            return ipcSendSync(\"dbBulkDocs\", { docs });\n        },\n        allDocs<T extends {} = Record<string, any>>(key?: string): DbDoc<T>[] {\n            return ipcSendSync(\"dbAllDocs\", { key });\n        },\n        postAttachment(\n            docId: string,\n            attachment: Buffer | Uint8Array,\n            type: string,\n        ): DbReturn {\n            return ipcSendSync(\"dbPostAttachment\", {\n                docId,\n                attachment,\n                type,\n            });\n        },\n        getAttachment(docId: string): Uint8Array | null {\n            return ipcSendSync(\"dbGetAttachment\", { docId });\n        },\n        getAttachmentType(docId: string): string | null {\n            return ipcSendSync(\"dbGetAttachmentType\", { docId });\n        },\n    },\n    dbStorage: {\n        setItem(key: string, value: any) {\n            return ipcSendSync(\"dbStorageSetItem\", {\n                key,\n                value: JSON.parse(JSON.stringify(value)),\n            });\n        },\n        getItem(key: string) {\n            return ipcSendSync(\"dbStorageGetItem\", { key });\n        },\n        removeItem(key: string) {\n            return ipcSendSync(\"dbStorageRemoveItem\", { key });\n        },\n    },\n\n    view: {\n        setHeight(height: number) {\n            ipcSendToHost(\"view.setHeight\", { height }).then();\n        },\n        getHeight(): Promise<number> {\n            return ipcSendToHost(\"view.getHeight\", {}, true);\n        },\n    },\n\n    fad: {\n        async read(type: string, path: string): Promise<any> {\n            const fileData = await ipcSendAsync(\"fileRead\", { path });\n            if (!fileData) {\n                throw t(\"file.notFoundOrReadFailed\");\n            }\n            const fileDataJson = JSON.parse(fileData);\n            if (fileDataJson[\"type\"] !== type) {\n                throw t(\"file.unsupportedType\");\n            }\n            return fileDataJson[\"data\"];\n        },\n        async write(type: string, path: string, data: any): Promise<void> {\n            const fileData = {\n                type,\n                data,\n            };\n            const fileDataJson = JSON.stringify(fileData, null, 2);\n            await ipcSendAsync(\"fileWrite\", { path, data: fileDataJson });\n        },\n    },\n\n    detach: {\n        setTitle(title: string) {\n            ipcSend(\"detachSetTitle\", { title });\n        },\n        setOperates(\n            operates: {\n                name: string;\n                title: string;\n                click: () => void;\n            }[],\n        ) {\n            const cleanOperates = operates.map((o) => {\n                return {\n                    name: o.name,\n                    title: o.title,\n                };\n            });\n            if (!(\"onDetachOperateClick\" in FocusAny.hooks)) {\n                FocusAny.hooks.onDetachOperateClick = (payload: {\n                    name: string;\n                }) => {\n                    const { name } = payload;\n                    FocusAny.hooks.detachOperates.forEach((o) => {\n                        if (o.name === name) {\n                            o.click();\n                        }\n                    });\n                };\n            }\n            FocusAny.hooks.detachOperates = operates.map((o) => {\n                return {\n                    name: o.name,\n                    title: o.title,\n                    click: o.click,\n                };\n            });\n            ipcSend(\"detachSetOperates\", { operates: cleanOperates });\n        },\n        setPosition(\n            position:\n                | \"center\"\n                | \"right-bottom\"\n                | \"left-top\"\n                | \"right-top\"\n                | \"left-bottom\",\n        ) {\n            ipcSend(\"detachSetPosition\", { position });\n        },\n        setAlwaysOnTop(alwaysOnTop: boolean) {\n            ipcSend(\"detachSetAlwaysOnTop\", { alwaysOnTop });\n        },\n        setSize(width: number, height: number) {\n            ipcSend(\"detachSetSize\", { width, height });\n        },\n    },\n\n    util: {\n        randomString(length: number): string {\n            return StrUtil.randomString(length);\n        },\n        bufferToBase64(buffer: Buffer): string {\n            return FileUtil.bufferToBase64(buffer);\n        },\n        base64ToBuffer(base64: string): Buffer {\n            return FileUtil.base64ToBuffer(base64);\n        },\n        datetimeString(): string {\n            return TimeUtil.datetimeString();\n        },\n        base64Encode(data: any): string {\n            return EncodeUtil.base64Encode(data);\n        },\n        base64Decode(data: string): any {\n            return EncodeUtil.base64Decode(data);\n        },\n        md5(data: string): string {\n            return EncodeUtil.md5(data);\n        },\n        save(\n            filename: string,\n            data: string | Uint8Array,\n            option?: {\n                isBase64?: boolean;\n            },\n        ): boolean {\n            const path = FocusAny.showSaveDialog({\n                defaultPath: filename,\n            });\n            if (!path) {\n                return false;\n            }\n            if (option?.isBase64) {\n                // remove prefix data:image/svg+xml;base64,\n                if ((data as string).startsWith(\"data:\")) {\n                    data = (data as string).split(\",\")[1];\n                }\n                data = Buffer.from(data as string, \"base64\");\n            }\n            fs.writeFileSync(path, data as Uint8Array);\n            return true;\n        },\n    },\n};\n"
  },
  {
    "path": "electron/preload/index.ts",
    "content": "import { ipcRenderer, webFrame } from \"electron\";\nimport { MAPI } from \"../mapi/render\";\nimport { FocusAny } from \"./focusany\";\n\nwebFrame.setZoomLevel(1);\nwebFrame.setVisualZoomLevelLimits(1, 1);\nwebFrame.setZoomFactor(1);\n\n// @ts-ignore\nwindow[\"focusany\"] = FocusAny;\n\nMAPI.init();\n\nwindow[\"__page\"] = {\n    hooks: {},\n    onShow: (cb: Function) => {\n        window[\"__page\"].hooks.onShow = cb;\n    },\n    onHide: (cb: Function) => {\n        window[\"__page\"].hooks.onHide = cb;\n    },\n    onMaximize: (cb: Function) => {\n        window[\"__page\"].hooks.onMaximize = cb;\n    },\n    onUnmaximize: (cb: Function) => {\n        window[\"__page\"].hooks.onUnmaximize = cb;\n    },\n    onEnterFullScreen: (cb: Function) => {\n        window[\"__page\"].hooks.onEnterFullScreen = cb;\n    },\n    onLeaveFullScreen: (cb: Function) => {\n        window[\"__page\"].hooks.onLeaveFullScreen = cb;\n    },\n    broadcastListeners: {},\n    onBroadcast: (type: string, cb: (data: any) => void) => {\n        if (!(type in window[\"__page\"].broadcastListeners)) {\n            window[\"__page\"].broadcastListeners[type] = [];\n        }\n        window[\"__page\"].broadcastListeners[type].push(cb);\n    },\n    offBroadcast: (type: string, cb: (data: any) => void) => {\n        if (!(type in window[\"__page\"].broadcastListeners)) {\n            return;\n        }\n        window[\"__page\"].broadcastListeners[type] = window[\n            \"__page\"\n        ].broadcastListeners[type].filter((c) => c !== cb);\n    },\n    callPage: {},\n    registerCallPage: (\n        name: string,\n        cb: (\n            resolve: (data: any) => void,\n            reject: (error: string) => void,\n            data: any,\n        ) => void,\n    ) => {\n        window[\"__page\"].callPage[name] = cb;\n    },\n    channel: {},\n    createChannel: (cb: (data: any) => void) => {\n        const channel = Math.random().toString(36).substring(2);\n        window[\"__page\"].channel[channel] = cb;\n        return channel;\n    },\n    destroyChannel: (channel: string) => {\n        delete window[\"__page\"].channel[channel];\n    },\n\n    //\n    onPluginInit: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginInit = cb;\n    },\n    onPluginInitReady: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginInitReady = cb;\n    },\n    onPluginAlreadyOpened: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginAlreadyOpened = cb;\n    },\n    onPluginExit: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginExit = cb;\n    },\n    onPluginDetached: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginDetached = cb;\n    },\n    onPluginState: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginState = cb;\n    },\n    onPluginCodeInit: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginCodeInit = cb;\n    },\n    onPluginCodeData: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginCodeData = cb;\n    },\n    onPluginCodeSetting: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginCodeSetting = cb;\n    },\n    onPluginCodeExit: (cb: Function) => {\n        window[\"__page\"].hooks.onPluginCodeExit = cb;\n    },\n    onSetSubInput: (cb: Function) => {\n        window[\"__page\"].hooks.onSetSubInput = cb;\n    },\n    onRemoveSubInput: (cb: Function) => {\n        window[\"__page\"].hooks.onRemoveSubInput = cb;\n    },\n    onSetSubInputValue: (cb: Function) => {\n        window[\"__page\"].hooks.onSetSubInputValue = cb;\n    },\n    onDetachSet: (cb: Function) => {\n        window[\"__page\"].hooks.onDetachSet = cb;\n    },\n    onDetachWindowClosed: (cb: Function) => {\n        window[\"__page\"].hooks.onDetachWindowClosed = cb;\n    },\n    ipcSendToHost: (channel: string, type: string, data?: any) => {\n        ipcRenderer.sendToHost(channel, {\n            type,\n            data,\n        });\n    },\n    ipcSend: (channel: string, type: string, data?: any) => {\n        ipcRenderer.send(channel, {\n            type,\n            data,\n        });\n    },\n};\n\nipcRenderer.removeAllListeners(\"MAIN_PROCESS_MESSAGE\");\nipcRenderer.on(\"MAIN_PROCESS_MESSAGE\", (_event: any, payload: any) => {\n    if (\"APP_READY\" === payload.type) {\n        MAPI.init(payload.data.AppEnv);\n    } else if (\"CALL_PAGE\" === payload.type) {\n        let { type, data, option } = payload.data;\n        option = Object.assign(\n            {\n                waitReadyTimeout: 10 * 1000,\n            },\n            option,\n        );\n        // console.log('CALL_PAGE', type, {type, data, option})\n        const resultEventName = `event:callPage:${payload.id}`;\n        const send = (code: number, msg: string, data?: any) => {\n            ipcRenderer.send(resultEventName, { code, msg, data });\n        };\n        if (!window[\"__page\"].callPage) {\n            console.warn(\"CALL_PAGE.Failed\", JSON.stringify(payload));\n            send(-1, \"error\");\n            return;\n        }\n        const callPageExecute = () => {\n            try {\n                const maybePromise = window[\"__page\"].callPage[type](\n                    (resultData: any) => {\n                        send(0, \"ok\", resultData);\n                    },\n                    (error: string) => {\n                        send(-1, error);\n                    },\n                    data,\n                );\n                if (maybePromise && typeof maybePromise.then === \"function\") {\n                    maybePromise.catch((e: any) => {\n                        console.error(\"CallPage.Error\", e);\n                        send(\n                            -1,\n                            \"CallPageExecuteError: \" +\n                                (e?.message || e.toString()),\n                        );\n                    });\n                }\n            } catch (e) {\n                console.error(\"CallPage.Error\", e);\n                send(\n                    -1,\n                    \"CallPageExecuteError: \" + (e?.message || e.toString()),\n                );\n            }\n        };\n        if (!window[\"__page\"].callPage[type]) {\n            if (option.waitReadyTimeout > 0) {\n                const start = Date.now();\n                const monitor = () => {\n                    setTimeout(() => {\n                        if (!window[\"__page\"].callPage[type]) {\n                            if (Date.now() - start > option.waitReadyTimeout) {\n                                console.warn(\"CALL_PAGE.Timeout\", type, {\n                                    type,\n                                    data,\n                                    option,\n                                });\n                                send(-1, \"timeout\");\n                                return;\n                            } else {\n                                monitor();\n                                return;\n                            }\n                        } else {\n                            callPageExecute();\n                        }\n                    }, 10);\n                };\n                monitor();\n                return;\n            }\n            console.warn(\"CALL_PAGE.NotFound\", type, { type, data, option });\n            send(-1, \"event not found\");\n            return;\n        }\n        callPageExecute();\n    } else if (\"CHANNEL\" === payload.type) {\n        const { channel, data } = payload.data;\n        if (!window[\"__page\"].channel || !window[\"__page\"].channel[channel]) {\n            return;\n        }\n        window[\"__page\"].channel[channel](data);\n    } else if (\"BROADCAST\" === payload.type) {\n        const { type, data } = payload.data;\n        if (window[\"__page\"].broadcastListeners[type]) {\n            window[\"__page\"].broadcastListeners[type].forEach(\n                (cb: Function) => {\n                    cb(data);\n                },\n            );\n        }\n    } else {\n        console.warn(\"UnknownMainProcessMessage\", JSON.stringify(payload));\n    }\n});\n"
  },
  {
    "path": "electron/preload/plugin.ts",
    "content": "import { FocusAny } from \"./focusany\";\nimport { ipcRenderer, webFrame } from \"electron\";\n\nwebFrame.setZoomLevel(1);\nwebFrame.setVisualZoomLevelLimits(1, 1);\nwebFrame.setZoomFactor(1);\n\n// @ts-ignore\nwindow[\"focusany\"] = FocusAny;\n\nwindow[\"__page\"] = {\n    hooks: {},\n    onShow: (cb: Function) => {\n        window[\"__page\"].hooks.onShow = cb;\n    },\n    onHide: (cb: Function) => {\n        window[\"__page\"].hooks.onHide = cb;\n    },\n    onMaximize: (cb: Function) => {\n        window[\"__page\"].hooks.onMaximize = cb;\n    },\n    onUnmaximize: (cb: Function) => {\n        window[\"__page\"].hooks.onUnmaximize = cb;\n    },\n    onEnterFullScreen: (cb: Function) => {\n        window[\"__page\"].hooks.onEnterFullScreen = cb;\n    },\n    onLeaveFullScreen: (cb: Function) => {\n        window[\"__page\"].hooks.onLeaveFullScreen = cb;\n    },\n    broadcastListeners: {},\n    onBroadcast: (type: string, cb: (data: any) => void) => {\n        if (!(type in window[\"__page\"].broadcastListeners)) {\n            window[\"__page\"].broadcastListeners[type] = [];\n        }\n        window[\"__page\"].broadcastListeners[type].push(cb);\n    },\n    offBroadcast: (type: string, cb: (data: any) => void) => {\n        if (!(type in window[\"__page\"].broadcastListeners)) {\n            return;\n        }\n        window[\"__page\"].broadcastListeners[type] = window[\n            \"__page\"\n        ].broadcastListeners[type].filter((c) => c !== cb);\n    },\n    callPage: {},\n    registerCallPage: (\n        name: string,\n        cb: (\n            resolve: (data: any) => void,\n            reject: (error: string) => void,\n            data: any,\n        ) => void,\n    ) => {\n        window[\"__page\"].callPage[name] = cb;\n    },\n    channel: {},\n    createChannel: (cb: (data: any) => void) => {\n        const channel = Math.random().toString(36).substring(2);\n        window[\"__page\"].channel[channel] = cb;\n        return channel;\n    },\n    destroyChannel: (channel: string) => {\n        delete window[\"__page\"].channel[channel];\n    },\n};\n\nipcRenderer.removeAllListeners(\"MAIN_PROCESS_MESSAGE\");\nipcRenderer.on(\"MAIN_PROCESS_MESSAGE\", (_event: any, payload: any) => {\n    if (\"APP_READY\" === payload.type) {\n    } else if (\"CALL_PAGE\" === payload.type) {\n        let { type, data, option } = payload.data;\n        option = Object.assign(\n            {\n                waitReadyTimeout: 10 * 1000,\n            },\n            option,\n        );\n        // console.log('CALL_PAGE', type, {type, data, option})\n        const resultEventName = `event:callPage:${payload.id}`;\n        const send = (code: number, msg: string, data?: any) => {\n            ipcRenderer.send(resultEventName, { code, msg, data });\n        };\n        if (!window[\"__page\"].callPage) {\n            send(-1, \"error\");\n            return;\n        }\n        const callPageExecute = () => {\n            try {\n                const maybePromise = window[\"__page\"].callPage[type](\n                    (resultData: any) => {\n                        send(0, \"ok\", resultData);\n                    },\n                    (error: string) => {\n                        send(-1, error);\n                    },\n                    data,\n                );\n                if (maybePromise && typeof maybePromise.then === \"function\") {\n                    maybePromise.catch((e: any) => {\n                        console.error(\"CallPage.Error\", e);\n                        send(\n                            -1,\n                            \"CallPageExecuteError: \" +\n                                (e?.message || e.toString()),\n                        );\n                    });\n                }\n            } catch (e) {\n                console.error(\"CallPage.Error\", e);\n                send(\n                    -1,\n                    \"CallPageExecuteError: \" + (e?.message || e.toString()),\n                );\n            }\n        };\n        if (!window[\"__page\"].callPage[type]) {\n            if (option.waitReadyTimeout > 0) {\n                const start = Date.now();\n                const monitor = () => {\n                    setTimeout(() => {\n                        if (!window[\"__page\"].callPage[type]) {\n                            if (Date.now() - start > option.waitReadyTimeout) {\n                                send(-1, \"timeout\");\n                                return;\n                            } else {\n                                monitor();\n                                return;\n                            }\n                        } else {\n                            callPageExecute();\n                        }\n                    }, 10);\n                };\n                monitor();\n                return;\n            }\n            send(-1, \"event not found\");\n            return;\n        }\n        callPageExecute();\n    } else if (\"CHANNEL\" === payload.type) {\n        const { channel, data } = payload.data;\n        if (!window[\"__page\"].channel || !window[\"__page\"].channel[channel]) {\n            return;\n        }\n        window[\"__page\"].channel[channel](data);\n    } else if (\"BROADCAST\" === payload.type) {\n        const { type, data } = payload.data;\n        if (window[\"__page\"].broadcastListeners[type]) {\n            window[\"__page\"].broadcastListeners[type].forEach(\n                (cb: Function) => {\n                    cb(data);\n                },\n            );\n        }\n    } else {\n        console.warn(\"UnknownMainProcessMessage\", JSON.stringify(payload));\n    }\n});\n"
  },
  {
    "path": "electron/resources/build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n  </dict>\n</plist>"
  },
  {
    "path": "electron-builder.json5",
    "content": "// @see https://www.electron.build/configuration/configuration\n{\n    \"$schema\": \"https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json\",\n    \"appId\": \"FocusAny\",\n    \"asar\": true,\n    \"npmRebuild\": true,\n    \"publish\": [],\n    \"productName\": \"FocusAny\",\n    \"directories\": {\n        \"output\": \"dist-release\",\n        \"buildResources\": \"electron/resources/build\"\n    },\n    \"afterPack\": \"./scripts/build_optimize.cjs\",\n    \"files\": [\n        \"dist\",\n        \"dist-electron\"\n    ],\n    //    \"extraResources\": [\n    //        {\n    //            \"from\": \"node_modules/ffmpeg-static\",\n    //            \"to\": \"bin/ffmpeg\",\n    //        }\n    //    ],\n    \"win\": {\n        icon: \"electron/resources/build/logo.ico\",\n        \"target\": [\n            {\n                \"target\": \"nsis\",\n                \"arch\": [\n                    \"x64\",\n                    \"arm64\"\n                ]\n            },\n        ],\n        \"artifactName\": \"${productName}-${version}-win-${arch}.${ext}\",\n        \"extraResources\": [\n            {\n                \"from\": \"electron/resources/extra\",\n                \"to\": \"extra\",\n                \"filter\": [\n                    \"common\",\n                    \"win\"\n                ]\n            }\n        ],\n        \"protocols\": [\n            {\n                \"name\": \"FocusAny Protocol\",\n                \"schemes\": [\n                    \"focusany\"\n                ]\n            }\n        ],\n        \"fileAssociations\": [\n            {\n                \"ext\": \"fad\",\n                \"name\": \"FocusAny Data File\",\n                \"role\": \"Editor\"\n            }\n        ]\n    },\n    \"nsis\": {\n        \"artifactName\": \"${productName}-${version}-win-setup-${arch}.${ext}\",\n        \"shortcutName\": \"${productName}\",\n        \"uninstallDisplayName\": \"${productName}\",\n        \"oneClick\": false,\n        \"perMachine\": false,\n        \"allowToChangeInstallationDirectory\": true,\n        \"deleteAppDataOnUninstall\": false\n    },\n    \"portable\": {\n        \"artifactName\": \"${productName}-${version}-win-portable-${arch}.${ext}\",\n        \"requestExecutionLevel\": \"user\"\n    },\n    \"appx\": {\n        \"identityName\": \"FocusAny\",\n        \"publisher\": \"CN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX\",\n        \"publisherDisplayName\": \"FocusAny\",\n        \"languages\": [\n            \"zh-CN\",\n            \"en-US\",\n            \"zh-TW\"\n        ],\n        \"artifactName\": \"${productName}-${version}-win-appx-${arch}.${ext}\",\n    },\n    \"mac\": {\n        \"icon\": \"logo.icns\",\n        \"target\": [\n            {\n                \"target\": \"dmg\",\n                \"arch\": [\n                    \"x64\",\n                    \"arm64\"\n                ]\n            }\n        ],\n        \"artifactName\": \"${productName}-${version}-mac-${arch}.${ext}\",\n        \"extraResources\": [\n            {\n                \"from\": \"electron/resources/extra\",\n                \"to\": \"extra\",\n                \"filter\": [\n                    \"common\",\n                    \"osx\"\n                ]\n            }\n        ],\n        \"x64ArchFiles\": \"Contents/Resources/extra/**/*\",\n        \"entitlementsInherit\": \"./entitlements.mac.plist\",\n        \"entitlements\": \"./entitlements.mac.plist\",\n        \"extendInfo\": {\n            \"NSDocumentsFolderUsageDescription\": \"Application requests access to the user's Documents folder.\",\n            \"NSDownloadsFolderUsageDescription\": \"Application requests access to the user's Downloads folder.\",\n            \"NSAccessibilityUsageDescription\": \"Application requests access to the user's Accessibility features.\",\n        },\n        \"protocols\": [\n            {\n                \"name\": \"FocusAny Protocol\",\n                \"schemes\": [\n                    \"focusany\"\n                ]\n            }\n        ],\n        \"fileAssociations\": [\n            {\n                \"ext\": \"fad\",\n                \"name\": \"FocusAny Data File\",\n                \"role\": \"Editor\"\n            }\n        ],\n        \"electronLanguages\": [\n            \"zh-CN\",\n            \"en-US\"\n        ],\n        \"type\": \"development\",\n        \"notarize\": false,\n        \"darkModeSupport\": false,\n        \"hardenedRuntime\": true,\n        \"gatekeeperAssess\": false,\n        \"identity\": \"Xi'an Yanyi Information Technology Co., Ltd (Q96H3H33RK)\",\n    },\n    \"linux\": {\n        \"icon\": \"logo.icns\",\n        \"maintainer\": \"FocusAny\",\n        \"category\": \"Utility\",\n        \"target\": [\n            {\n                \"target\": \"AppImage\",\n                \"arch\": [\n                    \"x64\",\n                    \"arm64\"\n                ]\n            },\n            {\n                \"target\": \"deb\",\n                \"arch\": [\n                    \"x64\",\n                    \"arm64\"\n                ]\n            }\n        ],\n        \"artifactName\": \"${productName}-${version}-linux-${arch}.${ext}\",\n        \"extraResources\": [\n            {\n                \"from\": \"electron/resources/extra\",\n                \"to\": \"extra\",\n                \"filter\": [\n                    \"common\",\n                    \"linux\"\n                ]\n            }\n        ],\n        \"protocols\": [\n            {\n                \"name\": \"FocusAny Protocol\",\n                \"schemes\": [\n                    \"focusany\"\n                ]\n            }\n        ],\n        \"fileAssociations\": [\n            {\n                \"ext\": \"fad\",\n                \"name\": \"FocusAny Data File\",\n                \"role\": \"Editor\"\n            }\n        ]\n    },\n    \"afterSign\": \"./scripts/notarize.cjs\",\n}\n"
  },
  {
    "path": "entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>com.apple.security.app-sandbox</key>\n        <false/>\n        <key>com.apple.security.accessibility</key>\n        <true/>\n        <key>com.apple.security.cs.allow-jit</key>\n        <true/>\n        <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n        <true/>\n        <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n        <true/>\n        <key>com.apple.security.cs.disable-library-validation</key>\n        <true/>\n        <key>com.apple.security.device.input-monitor</key>\n        <true/>\n        <key>com.apple.security.automation.apple-events</key>\n        <true/>\n        <key>com.apple.security.device.audio-input</key>\n        <true/>\n    </dict>\n</plist>\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n    <style>\n        html,body{\n            padding:0;margin:0;\n            background: transparent !important;\n        }\n        #app-loading{\n            width:100vw;\n            height:100vh;\n            display: flex;\n            border-radius: 15px;\n            background-image: linear-gradient(130deg, #a8c8f4, #61c4f5, #ba59ff);\n            background-size: 300% 300%;\n            animation: border-flow-loading 2s linear infinite;\n            -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);\n        }\n        @keyframes border-flow-loading {\n            0% {\n                background-position: 0% 50%;\n            }\n            50% {\n                background-position: 100% 50%;\n            }\n            100% {\n                background-position: 0% 50%;\n            }\n        }\n        #app-loading .container{\n            margin:auto;\n            text-align: center;\n            height:100vh;\n            overflow:hidden;\n        }\n    </style>\n</head>\n<body>\n<div id=\"app\">\n    <div id=\"app-loading\">\n        <div class=\"container\">\n            <img src=\"/loading.svg\" style=\"height:60px;width:60px;margin:0 auto;\" />\n        </div>\n    </div>\n</div>\n<script type=\"module\" src=\"/src/main.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"focusany\",\n    \"version\": \"1.1.0\",\n    \"main\": \"dist-electron/main/index.js\",\n    \"description\": \"FocusAny\",\n    \"author\": \"ModStartLib\",\n    \"homepage\": \"https://focusany.com\",\n    \"license\": \"Apache-2.0\",\n    \"private\": true,\n    \"keywords\": [\n        \"electron\",\n        \"rollup\",\n        \"vite\",\n        \"vue3\",\n        \"vue\"\n    ],\n    \"debug\": {\n        \"env\": {\n            \"VITE_DEV_SERVER_URL\": \"http://127.0.0.1:3344/\"\n        }\n    },\n    \"type\": \"module\",\n    \"scripts\": {\n        \"dev\": \"vite\",\n        \"dev:seed\": \"tsx test/dev-seed.ts\",\n        \"dev:mac\": \"pkill -f focusany; vite\",\n        \"dev:mac:pre\": \"export ELECTRON_ENV_PROD=1 && pkill -f focusany; vite\",\n        \"dev:win\": \"chcp 65001 && vite\",\n        \"dev:win:pre\": \"chcp 65001 && set ELECTRON_ENV_PROD=1 && vite\",\n        \"dev:debug\": \"vite --debug\",\n        \"build:preview\": \"npm run format && vite build\",\n        \"build\": \"npm run format && vite build && electron-builder\",\n        \"build:win\": \"npm run format && vite build && electron-builder --win\",\n        \"build:mac\": \"npm run format && vite build && electron-builder --mac\",\n        \"build:mac-arm\": \"npm run format && vite build && electron-builder --mac --arm64\",\n        \"build:linux\": \"npm run format && vite build && electron-builder --linux\",\n        \"preview\": \"vite preview\",\n        \"format\": \"prettier --write --log-level warn \\\"src/**/*.{ts,tsx,vue,js}\\\" \\\"electron/**/*.{ts,tsx,js}\\\"\",\n        \"lint\": \"eslint src electron\",\n        \"postinstall\": \"electron-builder install-app-deps\",\n        \"postuninstall\": \"electron-builder install-app-deps\",\n        \"re-sqlite\": \"npx electron-rebuild -f -w better-sqlite3\"\n    },\n    \"devDependencies\": {\n        \"@arco-design/web-vue\": \"^2.55.3\",\n        \"@electron/notarize\": \"^2.5.0\",\n        \"@iconify-json/mdi\": \"^1.2.3\",\n        \"@rollup/plugin-commonjs\": \"^28.0.2\",\n        \"@types/lodash-es\": \"^4.17.12\",\n        \"@types/pouchdb\": \"^6.4.2\",\n        \"@types/splitpanes\": \"^2.2.6\",\n        \"@typescript-eslint/eslint-plugin\": \"^8.59.2\",\n        \"@typescript-eslint/parser\": \"^8.59.2\",\n        \"@vitejs/plugin-vue\": \"^5.0.4\",\n        \"autoprefixer\": \"^10.4.19\",\n        \"electron\": \"^29.1.1\",\n        \"electron-builder\": \"^24.13.3\",\n        \"eslint\": \"^10.3.0\",\n        \"eslint-config-prettier\": \"^10.1.8\",\n        \"eslint-plugin-vue\": \"^10.9.1\",\n        \"less\": \"^4.2.0\",\n        \"postcss\": \"^8.4.39\",\n        \"prettier\": \"^3.8.3\",\n        \"tailwindcss\": \"^3.4.4\",\n        \"tsx\": \"^4.21.0\",\n        \"typescript\": \"^5.4.2\",\n        \"unplugin-icons\": \"^23.0.1\",\n        \"vite\": \"^5.1.5\",\n        \"vite-plugin-electron\": \"^0.28.4\",\n        \"vite-plugin-electron-renderer\": \"^0.14.5\",\n        \"vite-plugin-html\": \"^3.2.2\",\n        \"vue\": \"^3.4.21\",\n        \"vue-eslint-parser\": \"^10.4.0\",\n        \"vue-tsc\": \"^2.0.6\"\n    },\n    \"dependencies\": {\n        \"@babel/runtime\": \"^7.24.8\",\n        \"@codemirror/commands\": \"^6.1.2\",\n        \"@codemirror/lang-json\": \"^6.0.1\",\n        \"@codemirror/lang-python\": \"^6.1.6\",\n        \"@codemirror/state\": \"^6.4.1\",\n        \"@devicefarmer/adbkit\": \"^3.2.6\",\n        \"@electron-toolkit/preload\": \"^3.0.1\",\n        \"@electron-toolkit/utils\": \"^3.0.0\",\n        \"@electron/remote\": \"^2.1.2\",\n        \"@gradio/client\": \"^1.7.0\",\n        \"@nut-tree-fork/nut-js\": \"^4.2.6\",\n        \"@types/showdown\": \"^2.0.6\",\n        \"@uiw/codemirror-theme-dracula\": \"^4.23.0\",\n        \"@uiw/codemirror-theme-quietlight\": \"^4.23.0\",\n        \"@vue-js-cron/light\": \"^4.0.9\",\n        \"@xterm/addon-fit\": \"^0.10.0\",\n        \"@xterm/xterm\": \"^5.5.0\",\n        \"axios\": \"^1.7.2\",\n        \"better-sqlite3\": \"^12.2.0\",\n        \"chardet\": \"^2.0.0\",\n        \"codemirror\": \"^6.0.1\",\n        \"crypto\": \"^1.0.1\",\n        \"date-and-time\": \"^3.4.1\",\n        \"dayjs\": \"^1.11.12\",\n        \"electron-context-menu\": \"^4.0.4\",\n        \"express\": \"^5.1.0\",\n        \"extract-file-icon\": \"^0.3.2\",\n        \"ffmpeg-static\": \"^5.2.0\",\n        \"fix-path\": \"^4.0.0\",\n        \"get-windows\": \"^9.2.0\",\n        \"iconv\": \"^3.0.1\",\n        \"iconv-lite\": \"^0.6.3\",\n        \"js-base64\": \"^3.7.7\",\n        \"lodash-es\": \"^4.17.21\",\n        \"memorystream\": \"^0.3.1\",\n        \"node-window-manager\": \"^2.2.4\",\n        \"nodejs-base64\": \"^2.0.0\",\n        \"original-fs\": \"^1.2.0\",\n        \"pinia\": \"^2.1.7\",\n        \"pinyin-match\": \"^1.2.6\",\n        \"pouchdb\": \"^9.0.0\",\n        \"pouchdb-load\": \"^1.4.6\",\n        \"pouchdb-replication-stream\": \"^1.2.9\",\n        \"qrcode\": \"^1.5.4\",\n        \"showdown\": \"^2.1.0\",\n        \"splitpanes\": \"^3.1.5\",\n        \"systeminformation\": \"^5.25.11\",\n        \"tiny-emitter\": \"^2.1.0\",\n        \"uiohook-napi\": \"^1.5.4\",\n        \"vue-command\": \"^35.2.1\",\n        \"vue-i18n\": \"^9.13.1\",\n        \"vue-router\": \"^4.4.0\",\n        \"wavesurfer.js\": \"^7.8.6\",\n        \"webdav\": \"^5.7.1\",\n        \"xgplayer\": \"^3.0.20\",\n        \"yauzl\": \"^3.1.3\"\n    },\n    \"optionalDependencies\": {\n        \"@electron/osx-sign\": \"^1.3.2\",\n        \"electron-clipboard-ex\": \"^1.3.3\",\n        \"node-mac-permissions\": \"^2.4.0\"\n    },\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/modstart-lib/focusany.git\"\n    }\n}\n"
  },
  {
    "path": "page/about.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/about.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/detachWindow.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/detachWindow.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/fastPanel.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/fastPanel.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/feedback.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/feedback.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/guide.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/guide.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/log.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/log.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/monitor.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/monitor.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/payment.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/payment.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/setup.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/setup.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/store.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/store.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/system.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/system.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/user.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/user.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "page/workflow.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>%name%</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script type=\"module\" src=\"/src/entry/workflow.ts\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n    plugins: {\n        tailwindcss: {},\n        autoprefixer: {},\n    },\n};\n"
  },
  {
    "path": "public/iconfont/iconfont.css",
    "content": "@font-face {\n  font-family: \"iconfont\"; /* Project id 4733566 */\n  src: url('iconfont.woff2?t=1736411931319') format('woff2'),\n       url('iconfont.woff?t=1736411931319') format('woff'),\n       url('iconfont.ttf?t=1736411931319') format('truetype');\n}\n\n.iconfont {\n  font-family: \"iconfont\" !important;\n  font-size: 16px;\n  font-style: normal;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.icon-backend:before {\n  content: \"\\e61c\";\n}\n\n.icon-command:before {\n  content: \"\\e6aa\";\n}\n\n.icon-view:before {\n  content: \"\\e6e1\";\n}\n\n.icon-code:before {\n  content: \"\\e6b0\";\n}\n\n.icon-info:before {\n  content: \"\\e72a\";\n}\n\n.icon-success:before {\n  content: \"\\e72d\";\n}\n\n.icon-text:before {\n  content: \"\\e959\";\n}\n\n.icon-close-o:before {\n  content: \"\\e6a7\";\n}\n\n.icon-pin:before {\n  content: \"\\e863\";\n}\n\n.icon-store:before {\n  content: \"\\e670\";\n}\n\n.icon-sound:before {\n  content: \"\\e62a\";\n}\n\n.icon-desktop:before {\n  content: \"\\e8e9\";\n}\n\n.icon-network:before {\n  content: \"\\e675\";\n}\n\n.icon-avatar:before {\n  content: \"\\e604\";\n}\n\n.icon-empty-box:before {\n  content: \"\\e620\";\n}\n\n.icon-github:before {\n  content: \"\\e732\";\n}\n\n.icon-gitee:before {\n  content: \"\\e601\";\n}\n\n.icon-close:before {\n  content: \"\\e61b\";\n}\n\n.icon-min:before {\n  content: \"\\e67a\";\n}\n\n.icon-max:before {\n  content: \"\\e665\";\n}\n\n"
  },
  {
    "path": "public/iconfont/iconfont.js",
    "content": "window._iconfont_svg_string_4733566='<svg><symbol id=\"icon-backend\" viewBox=\"0 0 1024 1024\"><path d=\"M187.7504 802.1504h648.4992a34.0992 34.0992 0 1 1 0 68.2496H187.7504a34.1504 34.1504 0 0 1 0-68.2496z m510.2592-51.2H325.9904a170.7008 170.7008 0 0 1-170.7008-170.7008v-256A170.6496 170.6496 0 0 1 325.9904 153.6h372.0192a170.6496 170.6496 0 0 1 170.7008 170.6496v256a170.6496 170.6496 0 0 1-170.7008 170.7008z m-444.416-499.0976a102.4 102.4 0 0 0-30.0032 72.3968v256a102.4 102.4 0 0 0 102.4 102.4h372.0192a102.4 102.4 0 0 0 102.4-102.4v-256a102.4 102.4 0 0 0-102.4-102.4H325.9904a102.4 102.4 0 0 0-72.448 30.0032z m120.0128 310.3232a34.1504 34.1504 0 0 1-24.064-9.8816l-64.512-68.3008a34.1504 34.1504 0 0 1 0-48.128l66.2016-66.56a34.1504 34.1504 0 1 1 48.4864 48.128l-44.3904 44.3904 42.3424 42.3424a34.1504 34.1504 0 0 1 0 48.128 34.1504 34.1504 0 0 1-24.064 9.8816z m261.8368-2.4576a34.0992 34.0992 0 0 1-11.1104-55.552l44.3392-44.3904-42.2912-42.3424a34.0992 34.0992 0 1 1 48.128-48.128l64.512 68.3008a34.1504 34.1504 0 0 1 0 48.128l-66.2528 66.56a34.048 34.048 0 0 1-37.376 7.424z m-170.5472 8.8576a34.0992 34.0992 0 0 1-25.2416-50.7904l85.3504-148.1216a34.2528 34.2528 0 1 1 59.392 34.1504L499.0464 551.936a34.2016 34.2016 0 0 1-34.1504 16.64z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-command\" viewBox=\"0 0 1024 1024\"><path d=\"M174.5 174.5h675c20.7 0 37.51875 16.7625 37.51875 37.51875v599.9625A37.51875 37.51875 0 0 1 849.5 849.5H174.5a37.51875 37.51875 0 0 1-37.51875-37.51875V212.01875C136.98125 191.2625 153.8 174.5 174.5 174.5z m37.51875 74.98125v525.0375h599.9625V249.48125H212.01875zM512 624.5h225v74.98125H512V624.5zM387.0125 512L280.925 405.96875l53.04375-53.1L493.04375 512l-159.075 159.13125-53.04375-53.1L387.0125 512z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-view\" viewBox=\"0 0 1024 1024\"><path d=\"M690.03474277 406.81901269l58.13208634 105.18098731-108.59417315-9.58570049-60.04868731 110.77789219L544.24718633 513.57303388 309.57559385 748.20685185l-33.82066934-33.74332206 234.74804062-234.63381797-99.69740507-35.27858145L521.54657773 384.50334307l-9.58570049-108.7083958 105.14231368 58.16896171 130.45025478-57.55557744L690.03474277 406.81901269zM904.61485742 276.40743154l0 471.18603604c0 86.7749669-70.24867119 157.10008623-157.0614126 157.10008623L276.36830791 904.69355381c-86.65984424 0-156.98406445-70.32422021-156.98406445-157.10008623L119.38424346 276.40743154c0-86.7749669 70.32422021-157.10008623 156.98406445-157.10008623l471.18603604 0C834.36618535 119.30734531 904.61485742 189.63246552 904.61485742 276.40743154zM826.16104883 315.67390947c0-65.07268067-52.8391749-117.75895927-117.87318193-117.75895928L315.63388584 197.91494932c-65.03400703 0-117.79763291 52.68627862-117.79763291 117.75895927l0 392.65488018c0 65.07268067 52.76362588 117.75895927 117.79763291 117.75895928l392.65398018 0c65.03400703 0 117.87318193-52.68627862 117.87318193-117.75895928L826.16104883 315.67390947z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-code\" viewBox=\"0 0 1024 1024\"><path d=\"M743.74285888 882.78857422H280.25714112c-76.79958344 0-139.04571533-62.29248047-139.04571534-139.04571534V280.25714112a139.04571533 139.04571533 0 0 1 139.04571534-139.04571534h463.48571776c76.75323487 0 139.04571533 62.2461319 139.04571534 139.04571534v463.48571776c0 76.75323487-62.29248047 139.04571533-139.04571534 139.04571534z m46.34857179-602.5314331a46.34857177 46.34857177 0 0 0-46.34857179-46.34857178H280.25714112C254.67272949 233.90856933 233.90856933 254.67272949 233.90856933 280.25714112v463.48571776a46.34857177 46.34857177 0 0 0 46.34857179 46.34857179h463.48571776c25.63076019 0 46.34857177-20.71781159 46.34857179-46.34857179V280.25714112z m-92.69714356 278.09143066v46.34857178c0 76.75323487-62.29248047 139.04571533-139.04571533 139.04571532v-92.69714355c25.63076019 0 46.34857177-20.71781159 46.34857178-46.34857177v-46.34857178a46.34857177 46.34857177 0 1 0 0-92.69714355v-46.34857178a46.34857177 46.34857177 0 0 0-46.34857178-46.34857178V280.25714112c76.75323487 0 139.04571533 62.2461319 139.04571533 139.04571532v46.34857178a46.34857177 46.34857177 0 1 1 0 92.69714355z m-370.78857422 46.34857178v-46.34857179a46.36402106 46.36402106 0 0 1 0-92.69714354v-46.34857178a139.04571533 139.04571533 0 0 1 139.04571533-139.04571533v92.69714355c-25.58441162 0-46.34857177 20.76416016-46.34857178 46.34857178v46.34857178a46.36402106 46.36402106 0 0 0 0 92.69714355v46.34857178a46.34857177 46.34857177 0 0 0 46.34857178 46.34857177v92.69714355c-76.79958344 0-139.04571533-62.29248047-139.04571533-139.04571532z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-info\" viewBox=\"0 0 1024 1024\"><path d=\"M512 898.71874973C299.30468777 898.71874973 125.28125027 724.69531223 125.28125027 512S299.30468777 125.28125027 512 125.28125027s386.71874973 174.0234375 386.71874973 386.71874973-174.0234375 386.71874973-386.71874973 386.71874973z m0-696.09375c-170.15625027 0-309.37500027 139.21875-309.37500027 309.37500027 0 170.15625027 139.21875 309.37500027 309.37500027 309.37500027 170.15625027 0 309.37500027-139.21875 309.37500027-309.37500027 0-170.15625027-139.21875-309.37500027-309.37500027-309.37500027z\" fill=\"#8a8a8a\" ></path><path d=\"M512 746.59765652a37.96875 37.96875 0 0 1-38.67187473-38.67187554v-221.6953125c0-21.9375 16.76953125-38.67187473 38.67187473-38.67187473 21.90234348 0 38.67187473 16.73437473 38.67187473 38.67187473v221.6953125c0 21.9375-16.76953125 38.67187473-38.67187473 38.67187554zM512 390.81640625a37.96875 37.96875 0 0 1-38.67187473-38.67187473V316.07421902c0-21.9375 16.76953125-38.67187473 38.67187473-38.67187554 21.90234348 0 38.67187473 16.73437473 38.67187473 38.67187554v36.0703125c0 21.9375-18.03515625 38.67187473-38.67187473 38.67187473z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-success\" viewBox=\"0 0 1024 1024\"><path d=\"M512 832c-176.448 0-320-143.552-320-320S335.552 192 512 192s320 143.552 320 320-143.552 320-320 320m0-704C300.256 128 128 300.256 128 512s172.256 384 384 384 384-172.256 384-384S723.744 128 512 128\" fill=\"#8a8a8a\" ></path><path d=\"M619.072 429.088l-151.744 165.888-62.112-69.6a32 32 0 1 0-47.744 42.624l85.696 96a32 32 0 0 0 23.68 10.688h0.192c8.96 0 17.536-3.776 23.616-10.4l175.648-192a32 32 0 0 0-47.232-43.2\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-text\" viewBox=\"0 0 1024 1024\"><path d=\"M775.67187475 907.5078125L248.32812525 907.5078125c-72.50976588 0-131.83593775-59.32617188-131.83593775-131.83593775L116.4921875 248.32812525c0-72.50976588 59.32617188-131.83593775 131.83593775-131.83593775l527.34375026 0c72.50976588 0 131.83593775 59.32617188 131.83593777 131.83593775l0 527.34375026C907.5078125 848.18164063 848.18164063 907.5078125 775.67187475 907.5078125zM248.32812525 182.410156C212.07324193 182.410156 182.410156 212.07324193 182.410156 248.32812525l0 527.34375026c0 36.25488256 29.66308594 65.9179685 65.91796849 65.91796849l527.34375026 0c36.25488256 0 65.9179685-29.66308594 65.91796849-65.91796849L841.589844 248.32812525c0-36.25488256-29.66308594-65.9179685-65.91796849-65.91796849L248.32812525 182.410156z\" fill=\"#8a8a8a\" ></path><path d=\"M676.79492162 380.16406225L347.20507838 380.16406225C327.42968775 380.16406225 314.24609375 366.980469 314.24609375 347.20507838s13.183594-32.95898463 32.95898463-32.95898463l329.589844 0c19.77539063 0 32.95898463 13.183594 32.95898465 32.95898463S696.57031225 380.16406225 676.79492162 380.16406225z\" fill=\"#8a8a8a\" ></path><path d=\"M347.20507838 413.12304687C327.42968775 413.12304687 314.24609375 399.93945287 314.24609375 380.16406225L314.24609375 347.20507838c0-19.77539063 13.183594-32.95898463 32.95898463-32.95898463s32.95898463 13.183594 32.95898464 32.95898463l0 32.95898464C380.16406225 399.93945287 366.980469 413.12304687 347.20507838 413.12304687z\" fill=\"#8a8a8a\" ></path><path d=\"M676.79492162 413.12304687c-19.77539063 0-32.95898463-13.183594-32.95898464-32.95898462L643.83593775 347.20507838c0-19.77539063 13.183594-32.95898463 32.95898464-32.95898463s32.95898463 13.183594 32.95898464 32.95898463l0 32.95898464C709.75390625 399.93945287 696.57031225 413.12304687 676.79492162 413.12304687z\" fill=\"#8a8a8a\" ></path><path d=\"M512 709.75390625c-19.77539063 0-32.95898463-13.183594-32.95898463-32.95898463L479.04101537 347.20507838c0-19.77539063 13.183594-32.95898463 32.95898463-32.95898463s32.95898463 13.183594 32.95898463 32.95898463l0 329.589844C544.95898463 696.57031225 531.77539063 709.75390625 512 709.75390625z\" fill=\"#8a8a8a\" ></path><path d=\"M544.95898463 709.75390625l-65.91796849 0c-19.77539063 0-32.95898463-13.183594-32.95898464-32.95898463s13.183594-32.95898463 32.95898464-32.95898464l65.91796849 0c19.77539063 0 32.95898463 13.183594 32.95898464 32.95898464S564.73437525 709.75390625 544.95898463 709.75390625z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-close-o\" viewBox=\"0 0 1024 1024\"><path d=\"M512 128C300.8 128 128 300.8 128 512s172.8 384 384 384 384-172.8 384-384S723.2 128 512 128zM512 832c-179.2 0-320-140.8-320-320s140.8-320 320-320 320 140.8 320 320S691.2 832 512 832z\" fill=\"#8a8a8a\" ></path><path d=\"M672 352c-12.8-12.8-32-12.8-44.8 0L512 467.2 396.8 352C384 339.2 364.8 339.2 352 352S339.2 384 352 396.8L467.2 512 352 627.2c-12.8 12.8-12.8 32 0 44.8s32 12.8 44.8 0L512 556.8l115.2 115.2c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8L556.8 512l115.2-115.2C684.8 384 684.8 364.8 672 352z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-pin\" viewBox=\"0 0 1024 1024\"><path d=\"M632.17142862 176.94285752a64.28571416 64.28571416 0 0 1 19.92857167 13.56428496l168.36428585 168.53571474a64.28571416 64.28571416 0 0 1-19.45714307 104.20714249l-59.65714336 26.4214292-111.92142833 112.05-8.87142831 123.25714276a64.28571416 64.28571416 0 0 1-109.58571475 40.82142833l-108.68571387-108.79285693-185.20714336 186.06428525-45.55714218-45.36428555 185.31428554-186.17142832-112.43571416-112.52142861a64.28571416 64.28571416 0 0 1 40.69285664-109.54285752l126.7714292-9.47142861 109.9285708-110.05714249 25.82142891-59.5714289a64.28571416 64.28571416 0 0 1 84.55714278-33.4285708z m-25.56428555 58.99285634l-30.68571445 70.77857169-135.42857139 135.64285752-150.64285694 11.20714277 266.59285664 266.78571416 10.58571475-147.12857138 137.31428584-137.48571475 70.65-31.28571387-168.38571445-168.51428613z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-store\" viewBox=\"0 0 1024 1024\"><path d=\"M487.24121094 126.18212891H181.46621094c-8.85146484 0-16.09716797 7.24570312-16.09716797 16.09716796v305.77500001c0 8.85146484 7.24570312 16.09716797 16.09716797 16.09716796H487.24121094c8.85146484 0 16.09716797-7.24570312 16.09716797-16.09716796V142.27929687c0-8.85146484-7.24570312-16.09716797-16.09716797-16.09716796z m-52.30195313 269.56230468H233.76816406V194.57333984h201.17109375v201.17109375zM889.57548828 126.18212891H583.80048828c-8.85146484 0-16.09716797 7.24570312-16.09716797 16.09716796v305.77500001c0 8.85146484 7.24570312 16.09716797 16.09716797 16.09716796h305.775c8.85146484 0 16.09716797-7.24570312 16.09716797-16.09716796V142.27929687c0-8.85146484-7.23779297-16.09716797-16.09716797-16.09716796z m-52.29404297 269.56230468H636.11035156V194.57333984h201.17109375v201.17109375z m52.29404297 132.77197266H583.80048828c-8.85146484 0-16.09716797 7.24570312-16.09716797 16.09716797v305.775c0 8.85146484 7.24570312 16.09716797 16.09716797 16.09716797h305.775c8.85146484 0 16.09716797-7.24570312 16.09716797-16.09716797V544.61357422c0-8.85146484-7.23779297-16.09716797-16.09716797-16.09716797z m-52.29404297 269.57021484H636.11035156V596.91552734h201.17109375v201.17109375zM447.01015625 697.50107422H318.26445312V568.75537109c0-4.4296875-3.62285156-8.04462891-8.0446289-8.0446289h-48.28359375c-4.42177734 0-8.04462891 3.61494141-8.04462891 8.0446289v128.74570313H125.13798828c-4.42177734 0-8.04462891 3.62285156-8.0446289 8.05253906v48.27568359c0 4.42177734 3.62285156 8.05253906 8.0446289 8.05253907H253.89160156v128.74570312c0 4.42177734 3.62285156 8.04462891 8.04462891 8.04462891h48.28359375c4.42177734 0 8.04462891-3.61494141 8.0446289-8.04462891V761.87392578h128.74570313c4.4296875 0 8.05253906-3.62285156 8.05253906-8.05253906v-48.27568359c-0.00791016-4.42177734-3.62285156-8.04462891-8.05253906-8.04462891z m0 0\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-sound\" viewBox=\"0 0 1024 1024\"><path d=\"M510.565625 887.384375c-19.096875 0-38.41875-13.275-62.859375-34.03125l-166.528125-138.403125L205.746875 714.95C157.5125 714.921875 118.25 675.6875 118.25 627.425l0-231.24375c0-48.234375 39.2625-87.496875 87.496875-87.496875l75.43125 0 166.44375-138.31875c24.525-21.065625 43.509375-33.75 62.775-33.75 20.925 0 45.365625 14.259375 45.365625 54.365625l0 641.671875C555.7625 873.06875 531.40625 887.384375 510.565625 887.384375L510.565625 887.384375zM205.746875 367.015625c-16.059375 0-29.165625 13.10625-29.165625 29.165625l0 231.24375c0 16.0875 13.10625 29.165625 29.165625 29.165625l87.496875 0c7.734375 0 15.159375 3.065625 20.615625 8.55l175.05 146.953125c3.09375 3.065625 5.90625 5.653125 8.49375 7.81875L497.403125 203.75c-2.53125 2.1375-5.428125 4.725-8.55 7.846875l-174.99375 146.86875c-5.45625 5.484375-12.88125 8.55-20.615625 8.55L205.746875 367.015625 205.746875 367.015625zM657.4625 710.84375c-5.878125 0-11.840625-1.771875-16.9875-5.484375-13.078125-9.39375-16.059375-27.61875-6.69375-40.725l11.559375-15.80625c30.178125-40.921875 56.25-76.303125 56.25-137.025 0-62.915625-23.090625-93.459375-52.36875-132.103125-5.175-6.80625-10.321875-13.66875-15.440625-20.75625-9.39375-13.078125-6.4125-31.303125 6.69375-40.725 13.10625-9.45 31.303125-6.4125 40.725 6.69375 4.78125 6.69375 9.703125 13.190625 14.540625 19.603125 31.55625 41.709375 64.153125 84.825 64.153125 167.315625 0 79.875-35.8875 128.615625-67.584375 171.646875l-11.1375 15.24375C675.490625 706.653125 666.546875 710.84375 657.4625 710.84375L657.4625 710.84375zM756.6875 809.871875c-6.834375 0-13.696875-2.390625-19.209375-7.228125-12.09375-10.603125-13.33125-29.053125-2.728125-41.146875 58.528125-66.76875 112.66875-143.15625 112.66875-249.665625 0-106.509375-54.140625-182.925-112.66875-249.6375-10.603125-12.121875-9.39375-30.54375 2.728125-41.175 12.0375-10.63125 30.4875-9.421875 41.146875 2.671875 62.859375 71.71875 127.125 162 127.125 288.140625 0 126.140625-64.29375 216.45-127.125 288.1125C772.803125 806.525 764.759375 809.871875 756.6875 809.871875L756.6875 809.871875zM756.6875 809.871875\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-desktop\" viewBox=\"0 0 1024 1024\"><path d=\"M800.28125027 793.24999973L223.71874973 793.24999973C149.89062473 793.24999973 90.125 733.484375 90.125 659.65625L90.125 223.71874973C90.125 149.89062473 149.89062473 90.125 223.71874973 90.125l573.04687527 0C874.10937527 90.125 933.875 149.89062473 933.875 223.71874973l0 432.421875C933.875 733.484375 874.10937527 793.24999973 800.28125027 793.24999973zM223.71874973 160.43749973C188.56250027 160.43749973 160.43749973 188.56250027 160.43749973 223.71874973l0 432.421875c0 35.15625027 28.12499973 63.28125 63.28125 63.28125l573.04687527 0c35.15625027 0 63.28125-28.12499973 63.28125-63.28125L860.046875 223.71874973c0-35.15625027-28.12499973-63.28125-63.28125-63.28125L223.71874973 160.43749973z\" fill=\"#8a8a8a\" ></path><path d=\"M125.28125027 582.31249973l773.43750027 0 0 70.31249972-773.43750027 0 0-70.31249972Z\" fill=\"#8a8a8a\" ></path><path d=\"M652.62500027 933.875L371.37499973 933.875c-21.09375 0-35.15625027-14.06250027-35.15625028-35.15625027s14.06250027-35.15625027 35.15625028-35.15625028l281.24999973 0c21.09375 0 35.15625027 14.06250027 35.15625027 35.15625028S673.71875027 933.875 652.62500027 933.875z\" fill=\"#8a8a8a\" ></path><path d=\"M371.37499973 933.875c-21.09375 0-35.15625027-14.06250027-35.15625028-35.15625027s14.06250027-35.15625027 35.15625028-35.15625028c7.03124973 0 35.15625027-35.15625027 35.15625027-105.46874999 0-21.09375 14.06250027-35.15625027 35.15625027-35.15625029s35.15625027 14.06250027 35.15625028 35.15625029C476.84374973 860.046875 431.14062527 933.875 371.37499973 933.875z\" fill=\"#8a8a8a\" ></path><path d=\"M652.62500027 933.875c-59.76562473 0-105.46875-73.828125-105.46875-175.78124973 0-21.09375 14.06250027-35.15625027 35.15625028-35.15625027s35.15625027 14.06250027 35.15625028 35.15625027c0 70.31249973 28.12499973 105.46875 35.15625027 105.46875 21.09375 0 35.15625027 14.06250027 35.15625027 35.15625028S673.71875027 933.875 652.62500027 933.875z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-network\" viewBox=\"0 0 1025 1024\"><path d=\"M512.38988172 933.87107953A421.72244275 421.72244275 0 0 1 348.4782293 123.37325962a422.07662329 422.07662329 0 0 1 328.64698197 777.55075351 419.25141293 419.25141293 0 0 1-164.73532955 32.9470664z m0-777.55075433C316.35484006 156.32032519 156.56157095 316.11359512 156.56157095 512.14863677s159.7932691 355.82831077 355.82831077 355.82831077 355.82831077-159.7932691 355.82831076-355.82831076S709.2485997 156.32032519 512.38988172 156.32032519z\" fill=\"#8a8a8a\" ></path><path d=\"M512.38988172 933.87107953c-37.06544888 0-73.30722143-12.3551499-106.25428702-37.06544971s-56.83368823-53.53898209-79.89663538-93.07546078c-44.47853916-78.24928107-69.18883813-182.03253909-69.18883813-291.58153226s24.71029898-213.33225119 69.18883813-291.58153308c23.06294632-39.5364787 49.42059878-70.83619162 79.89663538-93.07546079S475.32443283 90.42619404 512.38988172 90.42619404s73.30722143 12.3551499 106.25428701 37.06544887 56.83368823 53.53898209 79.89663455 93.07546078c44.47853916 78.24928107 69.18883813 182.03253909 69.18883896 291.58153308s-24.71029898 213.33225119-69.18883896 291.58153227c-23.06294632 39.5364787-49.42059878 70.83619162-79.89663455 93.07546078s-68.36516181 37.06544888-106.25428701 37.0654497z m0-777.55075433c-102.13590371 0-189.44562854 163.08797607-189.44562855 355.82831157s87.30972483 355.82831077 189.44562855 355.82831077 189.44562854-163.08797607 189.44562854-355.82831077S615.34946259 156.32032519 512.38988172 156.32032519z\" fill=\"#8a8a8a\" ></path><path d=\"M786.67420444 821.02737857a32.94706558 32.94706558 0 0 1-32.94706558-32.94706558c0-7.41308945-12.3551499-26.35765247-59.30471806-44.47853916s-112.84370014-28.82868228-182.03253908-28.82868227-133.43561664 9.88412009-182.0325391 28.82868227-59.30471887 37.06544888-59.30471805 44.47853916a32.94706558 32.94706558 0 0 1-65.89413199 0c0-16.4735332 5.76573679-41.18383218 32.12338925-65.89413199 16.4735332-15.64985605 40.36015585-28.82868228 69.18883814-40.36015503 55.18633557-21.41559283 128.49355701-33.77074273 205.91916175-33.77074274s149.90914985 11.53147275 205.91916174 33.77074274c28.82868228 11.53147275 51.8916286 24.71029898 69.18883813 40.36015503 26.35765247 24.71029898 32.12338926 49.42059878 32.12338926 65.89413199a32.94706558 32.94706558 0 0 1-32.94706641 32.94706558zM512.38988172 391.89184637c-77.42560474 0-150.73282617-12.3551499-205.91916175-34.59441905-28.82868228-11.53147275-51.8916286-25.53397614-69.18883813-41.1838322-26.35765247-24.71029898-32.12338926-50.24427512-32.12338926-66.71780831a32.94706558 32.94706558 0 0 1 65.894132 0c0 11.53147275 18.12088586 30.47603576 59.30471804 46.94956815s112.0200238 29.65235943 181.20886277 29.65235942 133.43561664-10.70779642 181.20886193-29.65235942 59.30471887-35.4180954 59.30471805-46.94956815a32.94706558 32.94706558 0 1 1 65.89413199 0c0 16.4735332-5.76573679 42.00750935-32.12338925 66.71780831-16.4735332 15.64985605-40.36015585 29.65235943-69.18883814 41.1838322-53.53898209 22.23926917-126.84620352 34.59441907-204.27180825 34.59441906z\" fill=\"#8a8a8a\" ></path><path d=\"M512.38988172 925.63431292a32.94706558 32.94706558 0 0 1-32.94706559-32.94706557V131.61002622a32.94706558 32.94706558 0 0 1 65.89413117 0v761.07722113a32.94706558 32.94706558 0 0 1-32.94706558 32.94706558z\" fill=\"#8a8a8a\" ></path><path d=\"M892.92849228 545.09570236H131.85127115a32.94706558 32.94706558 0 0 1 0-65.89413199h761.07722113a32.94706558 32.94706558 0 1 1 0 65.89413199z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-avatar\" viewBox=\"0 0 1024 1024\"><path d=\"M496.29042969 540.66640625c114.5390625 0 207.75234375-91.58378906 207.75234375-204.17695312C704.04277344 223.89628906 610.8453125 132.3125 496.29042969 132.3125c-114.5390625 0-207.72070313 91.59960938-207.72070313 204.17695312 0 112.56152344 93.18164063 204.17695312 207.72070313 204.17695313z m53.97890625 19.64882812h-92.390625C313.26523438 560.31523438 163.953125 644.22617187 163.953125 786.32421875v46.92304688C163.953125 907.5078125 279.72617188 907.5078125 426.25390625 907.5078125h155.65605469C722.66328125 907.5078125 844.2265625 907.5078125 844.2265625 833.23144531v-7.76777344-39.15527343c0-142.05058594-149.31210938-225.99316406-293.95722656-225.99316407z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-empty-box\" viewBox=\"0 0 1024 1024\"><path d=\"M908.92848219 507.95346312l-0.14198344 0.73965938-0.01343063 0.01438969L806.53422594 390.59239438c-2.56242281-5.00780438-7.29777188-8.59289438-12.87543094-9.73068282L466.69999344 314.71360062a18.99512156 18.99512156 0 0 0-10.641105 0.88260094L192.27421437 417.62110906a18.43678031 18.43678031 0 0 0-4.19715468 2.50294313h-0.91042313a82.057005 82.057005 0 0 0-3.86905593 4.43795062L100.19296531 564.7987625c-2.94424406 4.978065-3.37211344 11.02388625-1.13874843 16.34443875 2.23336594 5.33398312 6.85647094 9.33255281 12.51855281 10.82530031l69.02232281 17.86692563v194.27883937c0 8.19476438 5.49131719 15.40715437 13.47118688 17.7105525l368.2847025 104.37339469a18.92412937 18.92412937 0 0 0 14.96489437-1.63569094L809.36430688 796.1487875c5.97482906-3.2857725 9.64434094-9.51674719 9.58678031-16.23219469l-2.39069907-192.96932718 87.73059844-51.08344501a18.52695844 18.52695844 0 0 0 8.93346282-12.88886156 18.33221063 18.33221063 0 0 0-4.29596719-15.02149594zM144.80367875 563.31848563l63.64420969-107.31763782 99.25142718 23.61438938 224.55015563 53.74372125-76.33352625 110.8739475-311.11226625-80.91442032z m73.517835 227.80810687V620.44871188l241.47787781 62.76256687c7.68247125 1.96378875 15.77650312-1.05240656 20.21445469-7.53856875l65.42380312-95.08305375 2.986455 303.99964781-330.10259062-93.46271156z m353.60473594-286.54428375l-254.50968188-60.67214344-56.87312062-13.59973969 204.05077406-78.38365593 273.95569781 56.10564-29.93074687 17.12822718-136.6929225 79.42167188z m13.62852 373.79041031l-3.00180469-305.74950187 57.94087594 96.10763906c2.51829281 4.21058531 6.62910563 7.25556094 11.43736593 8.47873125 4.80826031 1.20878062 9.89952844 0.51229313 14.1695925-1.97817844l113.17830563-65.83536375 1.94843906 160.7630025-195.67277437 108.21367125zM663.11146625 634.04845156h-0.01343062L601.12980969 531.44025969l126.35113406-73.8171525 57.94183594-33.94178719 80.52972094 92.40934687L663.11146625 634.04845156zM483.6708875 227.88095281c12.06286125 0 21.85014562-9.78728437 21.85014562-21.85014562V118.62926562c0-12.06382031-9.78728437-21.85014562-21.85014562-21.85014562-12.07725187 0-21.85014562 9.78728437-21.85014562 21.85014562v87.40154157c0 12.06286125 9.77289375 21.85014562 21.85014562 21.85014562z m-161.61682406 24.19767375c7.07040656 9.78728437 20.72674687 11.99186906 30.51403125 4.93681313 9.78728437-7.07040656 11.99186906-20.72674687 4.93681312-30.51403125l-51.15539719-70.85755875c-7.07040656-9.78728437-20.72578781-12.00625969-30.51403125-4.95024375-9.77289375 7.07040656-11.99186906 20.7411375-4.9224225 30.51403125l51.14100657 70.87098937z m293.84301468 0.76843875c9.03419438 7.16921906 21.63621094 4.92146344 28.1521125-5.021235l47.2009575-72.00973781c6.5149425-9.94365844 4.46673187-23.81393438-4.56650343-30.98315344-9.03419438-7.17017906-21.63908906-4.92146344-28.15211344 5.00780532l-47.2009575 72.00973687c-6.5149425 9.95708906-4.46673187 23.82640594 4.56650437 30.99658406z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-github\" viewBox=\"0 0 1024 1024\"><path d=\"M511.62332577 101.70772891C278.74455939 101.61356015 90.125 290.13895078 90.125 522.82938046 90.125 706.83468204 208.11816406 863.24860468 372.44224295 920.69140625c22.12960391 5.55594296 18.73953671-10.17020077 18.73953754-20.9054133v-72.98060779c-127.78669061 14.97279611-132.96596018-69.59054141-141.53529612-83.71582031C232.3194758 713.52064768 191.35616641 705.98716482 203.59807501 691.86188592c29.09807501-14.97279611 58.76116095 3.76674142 93.13267264 54.52357737 24.86049142 36.81989374 73.35728201 30.60477109 97.93526797 24.4838172 5.36760626-22.12960391 16.85616641-41.90499454 32.67647892-57.25446487-132.40094842-23.73046875-187.58370547-104.52706485-187.58370548-200.57896171 0-46.61342111 15.34946951-89.4601008 45.48339844-124.01995001-19.2103797-56.97195859 1.78920236-105.75125546 4.61425781-113.00223202 54.71191406-4.89676328 111.58970389 39.17410702 116.015625 42.65834298 31.07561407-8.38099924 66.57714844-12.80691952 106.31626639-12.80692035 39.92745548 0 75.5231586 4.61425781 106.88127814 13.08942581 10.64104376-8.09849295 63.37541876-45.95424142 114.22642346-41.33998361 2.7308875 7.25097656 23.25962576 54.90025076 5.17926874 111.11886173 30.51060233 34.65401798 46.04840936 77.87737189 46.04840936 124.58496093 0 96.24023438-55.55943045 177.13099924-188.3370531 200.48479377a120.06487189 120.06487189 0 0 1 35.87820859 85.69335937v105.93959217c0.75334845 8.47516718 0 16.85616641 14.1252789 16.85616641 166.77246094-56.21861014 286.83733282-213.76255545 286.83733282-399.36872187 0-232.78459845-188.71372732-421.21582031-421.40415784-421.21582031z\"  ></path></symbol><symbol id=\"icon-gitee\" viewBox=\"0 0 1024 1024\"><path d=\"M512 932A420 420 0 1 1 512 92a420 420 0 0 1 0 840z m212.58-466.68H486.08a20.76 20.76 0 0 0-20.76 20.76v51.84c0 11.46 9.3 20.76 20.7 20.76h145.2c11.52 0 20.76 9.3 20.76 20.7v10.38c0 34.38-27.84 62.22-62.22 62.22H392.72a20.76 20.76 0 0 1-20.7-20.7V434.24c0-34.38 27.84-62.22 62.16-62.22h290.4c11.4 0 20.7-9.3 20.7-20.76v-51.84a20.76 20.76 0 0 0-20.7-20.76h-290.4a155.52 155.52 0 0 0-155.52 155.58v290.34c0 11.46 9.3 20.76 20.76 20.76h305.88a139.98 139.98 0 0 0 140.04-139.98V486.08a20.76 20.76 0 0 0-20.76-20.76z\" fill=\"#C71D23\" ></path></symbol><symbol id=\"icon-close\" viewBox=\"0 0 1025 1024\"><path d=\"M863.63609223 252.92052971L604.55551029 512.00111164 863.63609223 771.08243443l0 0c11.8405248 11.85831034 19.15544685 28.20665232 19.15544685 46.27497604 0 36.1373883-29.29377545 65.43116374-65.43190461 65.43116375-18.06832372 0-34.4351921-7.31492206-46.27423517-19.17323167l0 0L512.00259335 604.53476061 252.92275299 863.6160834l0-1e-8c-11.8405248 11.85831034-28.20665232 19.17323167-46.2757169 19.17323168-36.13812915 0-65.43116374-29.29377545-65.43116375-65.43116375 0-18.06906457 7.31418049-34.41666643 19.15470527-46.27497603l0 0 259.08132281-259.08132279L160.36983677 252.92052971l0 0c-11.8405248-11.84126564-19.15470528-28.2073939-19.15470528-46.27645847 0-36.1373883 29.2930346-65.4304229 65.43116375-65.4304229 18.06906457 0 34.43593367 7.31418049 46.27571688 19.17249082l0 0 259.0798411 259.08058193L771.08465772 160.38613988l1e-8 0c11.83978394-11.85831034 28.20665232-19.17249082 46.27423517-19.17249081 36.13812915 0 65.43190461 29.2930346 65.43190461 65.4304229C882.7907975 224.71239495 875.47587544 241.07926407 863.63609223 252.92052971L863.63609223 252.92052971z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-min\" viewBox=\"0 0 1024 1024\"><path d=\"M128 448h768v128H128z\" fill=\"#8a8a8a\" ></path></symbol><symbol id=\"icon-max\" viewBox=\"0 0 1024 1024\"><path d=\"M817.859375 248.328125v527.34375H206.140625V248.328125h611.71875m87.890625-87.890625H118.25v703.125h787.5V160.4375z m0 157.76367188H118.25v87.89062499h787.5v-87.890625z\" fill=\"#8a8a8a\" ></path></symbol></svg>',(c=>{var a=(l=(l=document.getElementsByTagName(\"script\"))[l.length-1]).getAttribute(\"data-injectcss\"),l=l.getAttribute(\"data-disable-injectsvg\");if(!l){var t,i,o,e,h,s=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write(\"<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>\")}catch(a){console&&console.log(a)}}t=function(){var a,l=document.createElement(\"div\");l.innerHTML=c._iconfont_svg_string_4733566,(l=l.getElementsByTagName(\"svg\")[0])&&(l.setAttribute(\"aria-hidden\",\"true\"),l.style.position=\"absolute\",l.style.width=0,l.style.height=0,l.style.overflow=\"hidden\",l=l,(a=document.body).firstChild?s(l,a.firstChild):a.appendChild(l))},document.addEventListener?~[\"complete\",\"loaded\",\"interactive\"].indexOf(document.readyState)?setTimeout(t,0):(i=function(){document.removeEventListener(\"DOMContentLoaded\",i,!1),t()},document.addEventListener(\"DOMContentLoaded\",i,!1)):document.attachEvent&&(o=t,e=c.document,h=!1,n(),e.onreadystatechange=function(){\"complete\"==e.readyState&&(e.onreadystatechange=null,d())})}function d(){h||(h=!0,o())}function n(){try{e.documentElement.doScroll(\"left\")}catch(a){return void setTimeout(n,50)}d()}})(window);"
  },
  {
    "path": "public/iconfont/iconfont.json",
    "content": "{\n  \"id\": \"4733566\",\n  \"name\": \"FocusAny\",\n  \"font_family\": \"iconfont\",\n  \"css_prefix_text\": \"icon-\",\n  \"description\": \"\",\n  \"glyphs\": [\n    {\n      \"icon_id\": \"38207392\",\n      \"name\": \"backend\",\n      \"font_class\": \"backend\",\n      \"unicode\": \"e61c\",\n      \"unicode_decimal\": 58908\n    },\n    {\n      \"icon_id\": \"35089476\",\n      \"name\": \"command\",\n      \"font_class\": \"command\",\n      \"unicode\": \"e6aa\",\n      \"unicode_decimal\": 59050\n    },\n    {\n      \"icon_id\": \"321908\",\n      \"name\": \"view\",\n      \"font_class\": \"view\",\n      \"unicode\": \"e6e1\",\n      \"unicode_decimal\": 59105\n    },\n    {\n      \"icon_id\": \"5428337\",\n      \"name\": \"code\",\n      \"font_class\": \"code\",\n      \"unicode\": \"e6b0\",\n      \"unicode_decimal\": 59056\n    },\n    {\n      \"icon_id\": \"19418435\",\n      \"name\": \"info\",\n      \"font_class\": \"info\",\n      \"unicode\": \"e72a\",\n      \"unicode_decimal\": 59178\n    },\n    {\n      \"icon_id\": \"7009016\",\n      \"name\": \"success\",\n      \"font_class\": \"success\",\n      \"unicode\": \"e72d\",\n      \"unicode_decimal\": 59181\n    },\n    {\n      \"icon_id\": \"924549\",\n      \"name\": \"text\",\n      \"font_class\": \"text\",\n      \"unicode\": \"e959\",\n      \"unicode_decimal\": 59737\n    },\n    {\n      \"icon_id\": \"257406\",\n      \"name\": \"close-o\",\n      \"font_class\": \"close-o\",\n      \"unicode\": \"e6a7\",\n      \"unicode_decimal\": 59047\n    },\n    {\n      \"icon_id\": \"34453257\",\n      \"name\": \"pin\",\n      \"font_class\": \"pin\",\n      \"unicode\": \"e863\",\n      \"unicode_decimal\": 59491\n    },\n    {\n      \"icon_id\": \"32804187\",\n      \"name\": \"store\",\n      \"font_class\": \"store\",\n      \"unicode\": \"e670\",\n      \"unicode_decimal\": 58992\n    },\n    {\n      \"icon_id\": \"663540\",\n      \"name\": \"sound\",\n      \"font_class\": \"sound\",\n      \"unicode\": \"e62a\",\n      \"unicode_decimal\": 58922\n    },\n    {\n      \"icon_id\": \"924409\",\n      \"name\": \"desktop\",\n      \"font_class\": \"desktop\",\n      \"unicode\": \"e8e9\",\n      \"unicode_decimal\": 59625\n    },\n    {\n      \"icon_id\": \"6537202\",\n      \"name\": \"network\",\n      \"font_class\": \"network\",\n      \"unicode\": \"e675\",\n      \"unicode_decimal\": 58997\n    },\n    {\n      \"icon_id\": \"30808030\",\n      \"name\": \"avatar\",\n      \"font_class\": \"avatar\",\n      \"unicode\": \"e604\",\n      \"unicode_decimal\": 58884\n    },\n    {\n      \"icon_id\": \"14027553\",\n      \"name\": \"empty-box\",\n      \"font_class\": \"empty-box\",\n      \"unicode\": \"e620\",\n      \"unicode_decimal\": 58912\n    },\n    {\n      \"icon_id\": \"7239764\",\n      \"name\": \"github\",\n      \"font_class\": \"github\",\n      \"unicode\": \"e732\",\n      \"unicode_decimal\": 59186\n    },\n    {\n      \"icon_id\": \"39287937\",\n      \"name\": \"gitee\",\n      \"font_class\": \"gitee\",\n      \"unicode\": \"e601\",\n      \"unicode_decimal\": 58881\n    },\n    {\n      \"icon_id\": \"1115039\",\n      \"name\": \"close\",\n      \"font_class\": \"close\",\n      \"unicode\": \"e61b\",\n      \"unicode_decimal\": 58907\n    },\n    {\n      \"icon_id\": \"1649166\",\n      \"name\": \"min\",\n      \"font_class\": \"min\",\n      \"unicode\": \"e67a\",\n      \"unicode_decimal\": 59002\n    },\n    {\n      \"icon_id\": \"1818719\",\n      \"name\": \"max\",\n      \"font_class\": \"max\",\n      \"unicode\": \"e665\",\n      \"unicode_decimal\": 58981\n    }\n  ]\n}\n"
  },
  {
    "path": "public/static/pluginEmpty.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\"\n          content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n    <title>Plugin Empty View</title>\n</head>\n<body>\n<div style=\"text-align:center;padding:5rem 0;color:#999;font-size:2rem;\">\n    Plugin Empty View\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "scripts/build_optimize.cjs",
    "content": "const common = require(\"./common.cjs\");\n\nconsole.log(\"BuildOptimize\", {\n    name: common.platformName(),\n    arch: common.platformArch(),\n});\n\nexports.default = async function (context) {\n    console.log(\"BuildOptimize.output\", {\n        context: context,\n        root: context.appOutDir,\n    });\n    // copy extra electron/resources/extra/[name]-[arch] to extra\n    const platformName = common.platformName();\n    const platformArch = common.platformArch();\n    const name = platformName + \"-\" + platformArch;\n\n    const srcDir = `electron/resources/extra/${name}`;\n    let destDir = null;\n    if (platformName === 'osx') {\n        destDir = common.pathResolve(\n            context.appOutDir,\n            `${context.packager.appInfo.productFilename}.app`,\n            \"Contents\",\n            \"Resources\",\n            \"extra\",\n            name\n        );\n    } else if (platformName === 'win') {\n        destDir = common.pathResolve(context.appOutDir, \"resources\", \"extra\", name);\n    } else if (platformName === 'linux') {\n        destDir = common.pathResolve(context.appOutDir, \"resources\", \"extra\", name);\n    }\n\n    console.log(\"BuildOptimize.copy\", {\n        platformName,\n        platformArch,\n        srcDir,\n        destDir,\n    });\n\n    if (srcDir && common.exists(srcDir)) {\n        console.log(`Copying from ${srcDir} to ${destDir}`);\n        common.copy(srcDir, destDir, true);\n        console.log(`Copy completed`);\n    } else {\n        console.log(`No matching source directory found for platform: ${platformName}-${platformArch}`);\n    }\n\n    // common.listFiles(context.appOutDir, true).forEach((p) => {\n    // console.log('BuildOptimize.path', (p.isDir ? 'D:' : 'F:') + p.path);\n    // })\n    // const localeDir = context.appOutDir + \"/AigcPanel.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/\";\n    // console.log(`localeDir: ${localeDir}`);\n    // fs.readdir(localeDir, function (err, files) {\n    //     if (!(files && files.length)) {\n    //         return;\n    //     }\n    //     for (let f of files) {\n    //         if (f.endsWith('.lproj')) {\n    //             if (!(f.startsWith(\"en\") || f.startsWith(\"zh\"))) {\n    //                 const p = localeDir + f;\n    //                 console.log(`removeFile: ${p}`);\n    //                 fs.rmdirSync(p, {recursive: true});\n    //             }\n    //         }\n    //     }\n    // });\n};\n"
  },
  {
    "path": "scripts/common.cjs",
    "content": "const fs = require(\"node:fs\");\nconst {resolve, join} = require(\"node:path\");\nconst crypto = require(\"node:crypto\");\n\nconst dir = (p) => {\n    p = p || ''\n    return join(__dirname, \"../\" + p)\n}\n\nconst distReleaseDir = (p) => {\n    if (p) {\n        return dir(\"dist-release/\" + p)\n    } else {\n        return dir(\"dist-release\")\n    }\n}\n\nfunction calcSha256File(filePath) {\n    return new Promise((resolve, reject) => {\n        const hash = crypto.createHash(\"sha256\");\n        const stream = fs.createReadStream(filePath);\n        stream.on(\"data\", (data) => hash.update(data));\n        stream.on(\"end\", () => resolve(hash.digest(\"hex\")));\n        stream.on(\"error\", reject);\n    });\n}\n\n\nconst platformName = () => {\n    switch (process.platform) {\n        case \"darwin\":\n            return \"osx\";\n        case \"win32\":\n            return \"win\";\n        case \"linux\":\n            return \"linux\";\n    }\n    return null;\n}\n\nconst platformArch = () => {\n    switch (process.arch) {\n        case \"x64\":\n            return \"x86\";\n        case \"arm64\":\n            return \"arm64\";\n    }\n    return null;\n}\n\nconst listFiles = (dir, recursive, regex) => {\n    regex = regex || null\n    recursive = recursive || false\n    const files = fs.readdirSync(dir);\n    const list = [];\n    for (let f of files) {\n        const p = resolve(dir, f);\n        if (regex) {\n            if (!regex.test(p)) {\n                continue;\n            }\n        }\n        const stat = fs.statSync(p);\n        list.push({\n            isDir: stat.isDirectory(),\n            name: f,\n            path: p\n        });\n        if (recursive && stat.isDirectory()) {\n            list.push(...listFiles(p, recursive));\n        }\n    }\n    return list;\n}\n\nconst copy = (src, dest, print) => {\n    print = print || false\n    if (!fs.existsSync(src)) {\n        console.warn(`Source path does not exist: ${src}`);\n        return;\n    }\n    if (fs.statSync(src).isDirectory()) {\n        fs.mkdirSync(dest, {recursive: true});\n        const files = fs.readdirSync(src);\n        for (const file of files) {\n            copy(join(src, file), join(dest, file));\n        }\n    } else {\n        if (print) {\n            console.log(`Copying file from ${src} to ${dest}`);\n        }\n        fs.copyFileSync(src, dest);\n    }\n}\n\nconst pathResolve = (...args)=>{\n    return resolve(...args)\n}\n\nconst exists = (p) => {\n    try {\n        return fs.existsSync(p);\n    } catch (e) {\n        return false;\n    }\n}\n\nasync function calcSha256() {\n    console.log('calcSha256.start')\n    const results = []\n    const files = listFiles(distReleaseDir(), false, /\\.(exe|dmg|AppImage|deb)$/)\n    for (const p of files) {\n        const sha256 = await calcSha256File(p.path);\n        results.push({\n            name: p.name,\n            sha256: sha256\n        })\n    }\n    const target = distReleaseDir(`sha256-${platformName()}-${platformArch()}.yml`)\n    const content = results.map((r) => {\n        return `${r.name}: ${r.sha256}`\n    }).join(\"\\n\")\n    fs.writeFileSync(target, content);\n    console.log('calcSha256.end', target, results)\n}\n\nmodule.exports = {\n    dir,\n    distReleaseDir,\n    platformName,\n    platformArch,\n    listFiles,\n    copy,\n    pathResolve,\n    exists,\n    calcSha256,\n}\n"
  },
  {
    "path": "scripts/icon_convert.sh",
    "content": "#!/bin/bash\n\n# prepare\n# brew install --cask inkscape\n\necho \"Convert icon\"\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nPROJECT_ROOT=$(realpath \"${DIR}/..\")\necho \"PROJECT_ROOT: ${PROJECT_ROOT}\"\n\npath_svg=\"${PROJECT_ROOT}/public/logo.svg\"\npath_white_svg=\"${PROJECT_ROOT}/public/logo-white.svg\"\npath_build=\"${PROJECT_ROOT}/electron/resources/build\"\npath_extra=\"${PROJECT_ROOT}/electron/resources/extra\"\npath_source_png=\"${path_build}/logo_1024x1024.png\"\n\ncp -a \"${path_svg}\" \"${PROJECT_ROOT}/src/assets/image/logo.svg\"\ncp -a \"${path_white_svg}\" \"${PROJECT_ROOT}/src/assets/image/logo-white.svg\"\ncp -a \"${path_white_svg}\" \"${PROJECT_ROOT}/src/assets/image/search-icon.svg\"\n\ninkscape \"${path_svg}\" --export-type=png --export-filename=\"${path_source_png}\" -w 1024 -h 1024\n\nsize=(16 32 44 48 64 128 150 256 512)\nfor i in \"${size[@]}\"; do\n    path_png=\"${path_build}/logo@${i}x$i.png\"\n    echo \"Generate: logo@${i}x$i.png\"\n    inkscape --export-type=\"png\" --export-filename=\"${path_png}\" -w $i -h $i \"${path_source_png}\"\ndone\n\npath_ico=\"${path_build}/logo.ico\"\necho \"Generate: logo.ico\"\nmagick \"${path_source_png}\" -define icon:auto-resize=256,48,32,16 \"${path_ico}\"\n\necho \"Generate: logo.png\"\nrm -rf \"${path_build}/logo.png\"\ncp -a \"${path_build}/logo@256x256.png\" \"${path_build}/logo.png\"\n\necho \"Generate: logo.icns\"\npath_iconset=\"${path_build}/icon.iconset\"\nrm -rf \"${path_iconset}\"\nmkdir -p \"${path_iconset}\"\ncp -a \"${path_build}/logo@256x256.png\" \"${path_iconset}/icon_256x256.png\"\ncp -a \"${path_build}/logo@32x32.png\" \"${path_iconset}/icon_32x32.png\"\ncp -a \"${path_build}/logo@16x16.png\" \"${path_iconset}/icon_16x16.png\"\niconutil -c icns \"${path_iconset}\" -o \"${path_build}/logo.icns\"\n\necho \"Generate: appx/StoreLogo.png\"\ncp -a \"${path_build}/logo@256x256.png\" \"${path_build}/appx/StoreLogo.png\"\necho \"Generate: appx/Square44x44Logo.png\"\ncp -a \"${path_build}/logo@44x44.png\" \"${path_build}/appx/Square44x44Logo.png\"\necho \"Generate: appx/Square150x150Logo.png\"\ncp -a \"${path_build}/logo@150x150.png\" \"${path_build}/appx/Square150x150Logo.png\"\necho \"Generate: appx/Wide310x150Logo.png\"\nmagick \"${path_build}/logo@150x150.png\" -resize 310x150 -background none -gravity center -extent 310x150 \"${path_build}/appx/Wide310x150Logo.png\"\n\necho \"Generate: common/tray/icon.png\"\nmkdir -p \"${path_extra}/common/tray\"\ncp -a \"${path_build}/logo@256x256.png\" \"${path_extra}/common/tray/icon.png\"\necho \"Generate: common/tray/icon.ico\"\ncp -a \"${path_build}/logo.ico\" \"${path_extra}/common/tray/icon.ico\"\n\necho \"Generate: osx/tray/iconTemplate.png\"\nmkdir -p \"${path_extra}/osx/tray\"\nmagick \"${path_build}/logo-gray.png\" -resize 16x16 -background none -gravity center \"${path_extra}/osx/tray/iconTemplate.png\"\necho \"Generate: osx/tray/iconTemplate@2x.png\"\nmagick \"${path_build}/logo-gray.png\" -resize 32x32 -background none -gravity center \"${path_extra}/osx/tray/iconTemplate@2x.png\"\necho \"Generate: osx/tray/iconTemplate@4x.png\"\nmagick \"${path_build}/logo-gray.png\" -resize 64x64 -background none -gravity center \"${path_extra}/osx/tray/iconTemplate@4x.png\"\n\nrm -rf \"${path_iconset}\"\nrm -rf ${path_build}/logo@*\n"
  },
  {
    "path": "scripts/init.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nREPO_URL=\"https://github.com/modstart-lib/share-binary\"\nREPO_DIR=\"share-binary\"\n\nif [ ! -d \"$REPO_DIR/.git\" ]; then\n    echo \"🔹 目录不存在，正在克隆仓库...\"\n    git clone \"$REPO_URL\"\nelse\n    echo \"🔹 仓库已存在，进入目录并更新...\"\n    cd \"$REPO_DIR\"\n    git pull origin main\n    cd ..\nfi\n\n#rm -rfv electron/resources/extra/osx-arm64\n#mkdir -p electron/resources/extra/osx-arm64\n#cp -a share-binary/osx-arm64/scrcpy electron/resources/extra/osx-arm64/scrcpy\n#cp -a share-binary/osx-arm64/ffmpeg electron/resources/extra/osx-arm64/ffmpeg\n#cp -a share-binary/osx-arm64/ffprobe electron/resources/extra/osx-arm64/ffprobe\n#\n#rm -rfv electron/resources/extra/osx-x86\n#mkdir -p electron/resources/extra/osx-x86\n#cp -a share-binary/osx-x86/ffmpeg electron/resources/extra/osx-x86/ffmpeg\n#cp -a share-binary/osx-x86/ffprobe electron/resources/extra/osx-x86/ffprobe\n\n#rm -rfv electron/resources/extra/linux-arm64\n#mkdir -p electron/resources/extra/linux-arm64\n#cp -a share-binary/linux-arm64/scrcpy electron/resources/extra/linux-arm64/scrcpy\n#cp -a share-binary/linux-arm64/ffmpeg electron/resources/extra/linux-arm64/ffmpeg\n#cp -a share-binary/linux-arm64/ffprobe electron/resources/extra/linux-arm64/ffprobe\n\n#rm -rfv electron/resources/extra/linux-x86\n#mkdir -p electron/resources/extra/linux-x86\n#cp -a share-binary/linux-x86/scrcpy electron/resources/extra/linux-x86/scrcpy\n#cp -a share-binary/linux-x86/ffmpeg electron/resources/extra/linux-x86/ffmpeg\n#cp -a share-binary/linux-x86/ffprobe electron/resources/extra/linux-x86/ffprobe\n\nrm -rfv electron/resources/extra/win-x86\nmkdir -p electron/resources/extra/win-x86\ncp -a share-binary/win-x86/ScreenCapture.exe electron/resources/extra/win-x86/ScreenCapture.exe\n#cp -a share-binary/win-x86/ffmpeg.exe electron/resources/extra/win-x86/ffmpeg.exe\n#cp -a share-binary/win-x86/ffprobe.exe electron/resources/extra/win-x86/ffprobe.exe\n#\nls -R electron/resources/extra\n"
  },
  {
    "path": "scripts/notarize.cjs",
    "content": "const {notarize} = require(\"@electron/notarize\");\nconst common = require('./common.cjs')\n\nexports.default = async function notarizing(context) {\n    const appName = context.packager.appInfo.productFilename;\n    const {electronPlatformName, appOutDir} = context;\n    console.log(`  • Notarization Start`);\n    // We skip notarization if the process is not running on MacOS and\n    // if the enviroment variable SKIP_NOTARIZE is set to `true`\n    // This is useful for local testing where notarization is useless\n    if (\n        electronPlatformName !== \"darwin\" ||\n        process.env.SKIP_NOTARIZE === \"true\"\n    ) {\n        console.log(`  • Skipping notarization`);\n        return;\n    }\n\n    // THIS MUST BE THE SAME AS THE `appId` property\n    // in your electron builder configuration\n    const appId = \"FocusAny\";\n\n    let appPath = `${appOutDir}/${appName}.app`;\n    let {APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID} = process.env;\n    if (!APPLE_ID) {\n        console.info(\"  • Notarization ignore: APPLE_ID is empty\");\n        await common.calcSha256()\n        return;\n    }\n    const notarizeOption = {\n        tool: \"notarytool\",\n        appBundleId: appId,\n        appPath,\n        appleId: APPLE_ID,\n        appleIdPassword: APPLE_ID_PASSWORD,\n        teamId: APPLE_TEAM_ID,\n        verbose: true,\n    }\n    console.log(`  • Notarizing`, `appPath:${appPath} notarizeOption:${JSON.stringify(notarizeOption)}`);\n    try {\n        const result = await notarize(notarizeOption);\n        console.log(\"  • Notarization successful!\");\n        await common.calcSha256()\n        return result;\n    } catch (error) {\n        console.error(\"  • Notarization failed:\", error.message);\n        console.error(\"  • Stack trace:\", error.stack);\n        await common.calcSha256()\n        throw new Error(`Notarization failed: ${error.message}`);\n    }\n\n};\n"
  },
  {
    "path": "sdk/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"targets\": {\n          \"browsers\": [\n            \"> 1%\",\n            \"last 2 versions\",\n            \"not dead\"\n          ]\n        },\n        \"modules\": false,\n        \"useBuiltIns\": false\n      }\n    ],\n    [\n      \"@babel/preset-typescript\",\n      {\n        \"allowDeclareFields\": true\n      }\n    ]\n  ],\n  \"plugins\": []\n}\n"
  },
  {
    "path": "sdk/.github/workflows/tag-release.yml",
    "content": "name: Publish to npm\n\non:\n    push:\n        tags:\n            - \"v*.*.*\" # 仅当推送匹配 vX.X.X 的标签时触发\n\njobs:\n    publish:\n        runs-on: ubuntu-latest\n\n        steps:\n            - name: Checkout repository\n              uses: actions/checkout@v3\n\n            - name: Setup Node.js\n              uses: actions/setup-node@v3\n              with:\n                  node-version: \"20\"\n\n            - name: Install dependencies\n              run: npm install && npm run build\n\n            - name: Authenticate to npm\n              run: echo \"//registry.npmjs.org/:_authToken=${NPM_TOKEN}\" > ~/.npmrc\n              env:\n                  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n            - name: Publish package\n              run: npm publish --access public\n"
  },
  {
    "path": "sdk/.gitignore",
    "content": "*.tgz\n*.js\n\n"
  },
  {
    "path": "sdk/.npmignore",
    "content": "example/\n*.tgz\ntests/\n"
  },
  {
    "path": "sdk/.nvmrc",
    "content": "20\n"
  },
  {
    "path": "sdk/README.md",
    "content": "# FocusAny SDK\n\nTypeScript definitions and utilities for FocusAny.\n\n## Installation\n\n```bash\nnpm install focusany-sdk\n```\n\n## CLI Tools\n\n### FocusAny CLI\n\nThe SDK now provides a unified command-line interface through the `focusany` command.\n\n```bash\nnpx focusany <command> [options]\n```\n\nAvailable commands:\n\n- `release-prepare`: Check and update config.json for production release\n- `version`: Display the current version of FocusAny SDK\n- `help`: Show help information\n\n### Release Prepare\n\n#### Basic Usage\n\n```bash\nnpx focusany release-prepare\n```\n\nThis will check `dist/config.json` in your current directory.\n\n#### Custom Config Path\n\n```bash\nnpx focusany release-prepare path/to/your/config.json\n```\n\nThis command will:\n\n- Look for the specified config file (or `dist/config.json` by default)\n- Check if `development.env` is set to `\"dev\"`\n- Automatically change it to `\"prod\"` if needed\n- Display appropriate messages about the changes made\n\n#### Usage Examples\n\n**Release Prepare - Default config file:**\n\n```bash\nnpx focusany release-prepare\n```\n\n**Release Prepare - Custom config file:**\n\n```bash\nnpx focusany release-prepare build/config.json\nnpx focusany release-prepare src/configs/app-config.json\n```\n\n**Version Command:**\n\n```bash\nnpx focusany version\n```\n\n## Development\n\n### Building\n\n```bash\nnpm run build\n```\n\nThis will build both the shim files and the CLI tools.\n\n### Building CLI only\n\n```bash\nnpm run build:cli\n```\n\n### Building shim only\n\n```bash\nnpm run build:shim\n```\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "sdk/bin/command.ts",
    "content": "#!/usr/bin/env node\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as process from \"process\";\n\ninterface Config {\n    development?: {\n        env?: string;\n        [key: string]: any;\n    };\n    [key: string]: any;\n}\n\n// Command handler for release-prepare\nfunction releasePrepare(args: string[]) {\n    const customConfigPath = args[0];\n\n    // Get the project root directory that calls this command (not the SDK package directory)\n    const cwd = process.cwd();\n    const configPath = customConfigPath\n        ? path.resolve(cwd, customConfigPath)\n        : path.resolve(cwd, \"dist/config.json\");\n    const configDir = path.dirname(configPath);\n\n    console.log(\"🔍 Release Prepare\");\n    if (customConfigPath) {\n        console.log(`Using custom config file path: ${customConfigPath}`);\n    }\n    console.log(`Checking config file: ${configPath}`);\n\n    if (!fs.existsSync(configPath)) {\n        console.warn(`❌ Configuration file not found ${configPath}`);\n        process.exit(1);\n    }\n    let json: Config | null = null;\n    let jsonChanged = false;\n    try {\n        const configContent = fs.readFileSync(configPath, \"utf-8\");\n        json = JSON.parse(configContent);\n    } catch (error) {\n        console.error(\n            \"❌ Error reading or parsing configuration file:\",\n            (error as Error).message\n        );\n        process.exit(1);\n    }\n    if (!json) {\n        console.error(\"❌ Error parsing configuration file, json is null\");\n        process.exit(1);\n    }\n    if (json.development && json.development.env === \"dev\") {\n        console.warn(\n            `⚠️ Detected env field in config.json is \"dev\", it has been changed to \"prod\"`\n        );\n        json.development.env = \"prod\";\n        jsonChanged = true;\n    }\n    if (jsonChanged) {\n        fs.writeFileSync(configPath, JSON.stringify(json, null, 4), \"utf-8\");\n        console.log(\"✅ config.json file has been updated\");\n    }\n    console.log(\"🎉 Release prepare completed\");\n}\n\n// Command handler for version\nfunction version() {\n    try {\n        const packageJsonPath = path.resolve(__dirname, \"../package.json\");\n        const packageJson = JSON.parse(\n            fs.readFileSync(packageJsonPath, \"utf-8\")\n        );\n        console.log(`🔖 FocusAny SDK Version: ${packageJson.version}`);\n    } catch (error) {\n        console.error(\n            \"❌ Error reading version information:\",\n            (error as Error).message\n        );\n        process.exit(1);\n    }\n}\n\n// Command handler for help\nfunction showHelp() {\n    console.log(`\n🚀 FocusAny SDK CLI\n\nUsage:\n  npx focusany <command> [options]\n\nCommands:\n  release-prepare [path]  Check and update config.json for production release\n  version              Display the current version of FocusAny SDK\n  help                 Show this help message\n\nExamples:\n  npx focusany release-prepare\n  npx focusany release-prepare path/to/config.json\n  npx focusany version\n`);\n}\n\n// Main command router\nfunction main() {\n    const args = process.argv.slice(2);\n    const command = args.shift() || \"help\";\n\n    console.log(\"🚀 [FocusAny SDK] Start\");\n    switch (command) {\n        case \"release-prepare\":\n            releasePrepare(args);\n            break;\n        case \"version\":\n            version();\n            break;\n        case \"help\":\n            showHelp();\n            break;\n        default:\n            console.error(`❌ Unknown command: ${command}`);\n            showHelp();\n            process.exit(1);\n    }\n    console.log(\"🚀 [FocusAny SDK] End\");\n}\n\n// Execute the main function\nmain();\n"
  },
  {
    "path": "sdk/config.schema.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n    \"$id\": \"https://focusany.com/sdk/config.schema.json\",\n    \"title\": \"FocusAny 插件配置文件\",\n    \"description\": \"FocusAny 插件配置文件，用于描述插件的基本信息，以及插件的入口文件。\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"name\": {\n            \"type\": \"string\",\n            \"description\": \"插件名称，此为必选项，且插件应用内不可重复。\"\n        },\n        \"version\": {\n            \"type\": \"string\",\n            \"description\": \"插件版本，此为必选项。\"\n        },\n        \"platform\": {\n            \"type\": \"array\",\n            \"description\": \"支持的平台，此为选填，留空表示支持所有平台。\",\n            \"items\": {\n                \"type\": \"string\",\n                \"enum\": [\n                    \"win\",\n                    \"osx\",\n                    \"linux\"\n                ]\n            }\n        },\n        \"versionRequire\": {\n            \"type\": \"string\",\n            \"description\": \"FocusAny 版本要求，如 * 或 >=1.0.0 或 <=1.0.0 或 >1.0.0 或 <1.0.0，此为选填，留空表示不限制 FocusAny 版本。\"\n        },\n        \"editionRequire\": {\n            \"type\": \"array\",\n            \"description\": \"FocusAny 类型要求，此为选填，留空表示不限制 FocusAny 类型（open社区版、pro专业版）。\",\n            \"items\": {\n                \"type\": \"string\",\n                \"enum\": [\n                    \"open\",\n                    \"pro\"\n                ]\n            }\n        },\n        \"title\": {\n            \"type\": \"string\",\n            \"description\": \"插件标题，此为必选项。\"\n        },\n        \"logo\": {\n            \"type\": \"string\",\n            \"description\": \"插件图标，支持png,jpg,svg格式\"\n        },\n        \"description\": {\n            \"type\": \"string\",\n            \"description\": \"插件描述\"\n        },\n        \"main\": {\n            \"type\": \"string\",\n            \"description\": \"主入口文件\"\n        },\n        \"mainView\": {\n            \"type\": \"string\",\n            \"description\": \"快捷面板/智能视图 入口文件，当该配置为空时，使用主入口文件\"\n        },\n        \"preload\": {\n            \"type\": \"string\",\n            \"description\": \"插件预加载文件，相对于插件目录，需要是 cjs 文件\"\n        },\n        \"author\": {\n            \"type\": \"string\",\n            \"description\": \"插件作者\"\n        },\n        \"homepage\": {\n            \"type\": \"string\",\n            \"description\": \"插件主页\"\n        },\n        \"actions\": {\n            \"type\": \"array\",\n            \"description\": \"插件动作，描述了当 FocusAny 主输入框内容产生变化时，此插件应用是否显示在搜索结果列表中，一个插件应用可以有多个功能，一个功能可以提供多个命令供用户搜索。\",\n            \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\n                        \"type\": \"string\",\n                        \"description\": \"插件应用提供的某个功能的唯一标示，此为必选项，且插件应用内不可重复。\"\n                    },\n                    \"title\": {\n                        \"type\": \"string\",\n                        \"description\": \"对此功能的说明，将在搜索列表对应位置中显示。\"\n                    },\n                    \"icon\": {\n                        \"type\": \"string\",\n                        \"description\": \"此功能的图标，支持png,jpg,svg格式。\"\n                    },\n                    \"trackHistory\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"是否启用历史记录，默认为 true。启用后，FocusAny 会记录用户对该功能的使用历史，并在搜索结果中显示。\"\n                    },\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"description\": \"此功能的类型\",\n                        \"enum\": [\n                            \"command\",\n                            \"web\",\n                            \"code\",\n                            \"backend\",\n                            \"view\"\n                        ]\n                    },\n                    \"platform\": {\n                        \"type\": \"array\",\n                        \"description\": \"支持的平台，此为选填，留空表示支持所有平台。\",\n                        \"items\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"win\",\n                                \"osx\",\n                                \"linux\"\n                            ]\n                        }\n                    },\n                    \"data\": {\n                        \"type\": \"object\",\n                        \"description\": \"其他补充数据。\",\n                        \"properties\": {\n                            \"command\": {\n                                \"type\": \"string\",\n                                \"description\": \"type=command时，此为必选项，表示此功能的命令。\"\n                            },\n                            \"showFastPanel\": {\n                                \"type\": \"boolean\",\n                                \"description\": \"type=view时，是否在快捷面板中显示，默认为 false\"\n                            },\n                            \"showMainPanel\": {\n                                \"type\": \"boolean\",\n                                \"description\": \"type=view时，是否在主面板中显示，默认为 true\"\n                            }\n                        }\n                    },\n                    \"matches\": {\n                        \"type\": \"array\",\n                        \"description\": \"该功能下可响应的命令集，支持 6 种类型，由 matches 的类型或 matches.type 决定。\",\n                        \"items\": {\n                            \"oneOf\": [\n                                {\n                                    \"type\": \"string\",\n                                    \"description\": \"简单字符串匹配\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"required\": [\n                                        \"type\"\n                                    ],\n                                    \"properties\": {\n                                        \"type\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"类型\",\n                                            \"enum\": [\n                                                \"text\",\n                                                \"key\",\n                                                \"regex\",\n                                                \"file\",\n                                                \"image\",\n                                                \"window\",\n                                                \"editor\"\n                                            ]\n                                        },\n                                        \"name\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配名称\"\n                                        },\n                                        \"regex\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配正则表达式\"\n                                        },\n                                        \"title\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配标题\"\n                                        },\n                                        \"text\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配文本\"\n                                        },\n                                        \"minLength\": {\n                                            \"type\": \"number\",\n                                            \"description\": \"最小匹配长度\",\n                                            \"minimum\": 1\n                                        },\n                                        \"maxLength\": {\n                                            \"type\": \"number\",\n                                            \"description\": \"最大匹配长度\",\n                                            \"minimum\": 1,\n                                            \"maximum\": 10000\n                                        },\n                                        \"key\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配键值\"\n                                        },\n                                        \"minCount\": {\n                                            \"type\": \"number\",\n                                            \"description\": \"最小匹配数量\",\n                                            \"minimum\": 1\n                                        },\n                                        \"maxCount\": {\n                                            \"type\": \"number\",\n                                            \"description\": \"最大匹配数量\",\n                                            \"minimum\": 1,\n                                            \"maximum\": 10000\n                                        },\n                                        \"filterFileType\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"文件类型\",\n                                            \"enum\": [\n                                                \"file\",\n                                                \"directory\"\n                                            ]\n                                        },\n                                        \"filterExtensions\": {\n                                            \"type\": \"array\",\n                                            \"description\": \"文件扩展名\",\n                                            \"items\": {\n                                                \"type\": \"string\"\n                                            }\n                                        },\n                                        \"nameRegex\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配名称正则\"\n                                        },\n                                        \"titleRegex\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"匹配标题正则\"\n                                        },\n                                        \"attrRegex\": {\n                                            \"type\": \"object\",\n                                            \"description\": \"匹配属性正则\",\n                                            \"properties\": {\n                                                \"name\": {\n                                                    \"type\": \"string\",\n                                                    \"description\": \"属性名称\"\n                                                },\n                                                \"value\": {\n                                                    \"type\": \"string\",\n                                                    \"description\": \"属性值正则表达式\"\n                                                }\n                                            }\n                                        },\n                                        \"extensions\": {\n                                            \"type\": \"array\",\n                                            \"description\": \"文件扩展名\",\n                                            \"items\": {\n                                                \"type\": \"string\"\n                                            }\n                                        },\n                                        \"fadTypes\": {\n                                            \"type\": \"array\",\n                                            \"description\": \"FocusAny 数据类型\",\n                                            \"items\": {\n                                                \"type\": \"string\"\n                                            }\n                                        }\n                                    },\n                                    \"additionalProperties\": false\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"mcp\": {\n            \"type\": \"object\",\n            \"description\": \"MCP 配置，描述了插件应用在 MCP 中的相关信息。\",\n            \"properties\": {\n                \"tools\": {\n                    \"type\": \"array\",\n                    \"description\": \"插件应用提供的工具列表，一个插件应用可以提供多个工具。\",\n                    \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\n                                \"type\": \"string\",\n                                \"description\": \"工具名称，此为必选项，且插件应用内不可重复。建议使用小写+点号分隔，例如 'list.plugins'。\"\n                            },\n                            \"description\": {\n                                \"type\": \"string\",\n                                \"description\": \"工具描述，简要说明工具的用途。\"\n                            },\n                            \"inputSchema\": {\n                                \"type\": \"object\",\n                                \"description\": \"工具输入参数的 JSON Schema。必须符合 JSON Schema Draft 7 标准。\",\n                                \"properties\": {\n                                    \"type\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\n                                            \"object\"\n                                        ],\n                                        \"description\": \"输入参数必须是对象类型。\"\n                                    },\n                                    \"properties\": {\n                                        \"type\": \"object\",\n                                        \"description\": \"对象的属性定义。每个属性名对应一个参数。\",\n                                        \"additionalProperties\": {\n                                            \"type\": \"object\",\n                                            \"description\": \"参数的 JSON Schema 定义。例如 {\\\"type\\\":\\\"string\\\",\\\"description\\\":\\\"...\\\"}\",\n                                            \"properties\": {\n                                                \"type\": {\n                                                    \"type\": \"string\",\n                                                    \"description\": \"参数类型，例如 string、number、boolean、array、object 等。\",\n                                                    \"enum\": [\n                                                        \"string\",\n                                                        \"number\",\n                                                        \"object\",\n                                                        \"array\",\n                                                        \"boolean\"\n                                                    ]\n                                                },\n                                                \"description\": {\n                                                    \"type\": \"string\",\n                                                    \"description\": \"参数描述，简要说明参数的用途。\"\n                                                },\n                                                \"examples\": {\n                                                    \"type\": \"array\",\n                                                    \"description\": \"参数示例值。\",\n                                                    \"items\": {\n                                                        \"type\": [\n                                                            \"string\",\n                                                            \"number\",\n                                                            \"object\",\n                                                            \"array\",\n                                                            \"boolean\"\n                                                        ]\n                                                    }\n                                                }\n                                            }\n                                        }\n                                    },\n                                    \"required\": {\n                                        \"type\": \"array\",\n                                        \"description\": \"必填参数列表。\",\n                                        \"items\": {\n                                            \"type\": \"string\"\n                                        }\n                                    }\n                                },\n                                \"required\": [\n                                    \"type\",\n                                    \"properties\"\n                                ]\n                            }\n                        },\n                        \"required\": [\n                            \"name\",\n                            \"description\",\n                            \"inputSchema\"\n                        ]\n                    }\n                }\n            },\n            \"required\": [\n                \"tools\"\n            ]\n        },\n        \"development\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"env\": {\n                    \"type\": \"string\",\n                    \"description\": \"开发环境，prod 表示生产环境，dev 表示开发环境，prod 环境会忽略 development 的所有配置。\",\n                    \"enum\": [\n                        \"prod\",\n                        \"dev\"\n                    ]\n                },\n                \"main\": {\n                    \"type\": \"string\",\n                    \"description\": \"dev环境 入口文件，通常为开发环境如 http://localhost:8080\"\n                },\n                \"mainView\": {\n                    \"type\": \"string\",\n                    \"description\": \"dev环境 快捷面板/智能视图快速面板入口文件，通常为开发环境如 http://localhost:8080\"\n                },\n                \"showDevTools\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"dev环境 是否在插件加载完成后显示开发者窗口\"\n                },\n                \"showCodeDevTools\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"dev环境 是否在code执行完成后显示开发者窗口\"\n                },\n                \"keepCodeDevTools\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"dev环境 插件是否在code执行完成后保留开发者窗口\"\n                },\n                \"showViewDevTools\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"dev环境 是否在快捷面板/智能视图渲染view时显示开发者窗口\"\n                },\n                \"releaseDoc\": {\n                    \"type\": \"string\",\n                    \"description\": \"更新日志文档，参照插件选择根目录，默认为 release.md，使用 markdown 格式，格式为【## x.x.x 功能特性[换行][换行]更新内容详情】使用 --- 分割多个。\"\n                },\n                \"contentDoc\": {\n                    \"type\": \"string\",\n                    \"description\": \"插件内容文档，参照插件选择根目录，默认为 content.md，使用 markdown 格式。\"\n                },\n                \"previewDoc\": {\n                    \"type\": \"string\",\n                    \"description\": \"插件预览文档，参照插件选择根目录，默认为 preview.md，使用 markdown 格式，每行一个图片链接。\"\n                }\n            }\n        },\n        \"permissions\": {\n            \"type\": \"array\",\n            \"description\": \"插件权限\",\n            \"items\": {\n                \"type\": \"string\",\n                \"enum\": [\n                    \"ClipboardManage\",\n                    \"Api\",\n                    \"File\"\n                ]\n            }\n        },\n        \"setting\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"autoDetach\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"是否默认分离模式打开，默认为 false\"\n                },\n                \"detachPosition\": {\n                    \"type\": \"string\",\n                    \"description\": \"分离模式默认位置，默认为 center\",\n                    \"enum\": [\n                        \"center\",\n                        \"left-top\",\n                        \"right-top\",\n                        \"left-bottom\",\n                        \"right-bottom\"\n                    ]\n                },\n                \"detachAlwaysOnTop\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"分离模式默认是否置顶，默认为 false\"\n                },\n                \"height\": {\n                    \"type\": \"string\",\n                    \"description\": \"窗口高度，支持 数字 或 百分比，设置后窗口大小将默认为分离模式，默认为 600\"\n                },\n                \"width\": {\n                    \"type\": \"string\",\n                    \"description\": \"窗口宽度，支持 数字 或 百分比，设置后窗口大小将默认为分离模式，默认为 800\"\n                },\n                \"heightView\": {\n                    \"type\": \"integer\",\n                    \"description\": \"快速面板高度，单位为像素，默认为 100\"\n                },\n                \"singleton\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"是否只允许打开一个窗口，默认为 true\"\n                },\n                \"zoom\": {\n                    \"type\": \"number\",\n                    \"description\": \"窗口缩放比例，100表示原始大小，默认为 100\"\n                },\n                \"darkModeSupport\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"是否支持暗黑模式，默认为 false\"\n                },\n                \"httpEntry\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"是否使用 http 协议入口，默认为 false\"\n                },\n                \"remoteWebCacheEnable\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"是否启用远程Web缓存，默认为 false\"\n                },\n                \"moreMenu\": {\n                    \"type\": \"array\",\n                    \"description\": \"分离模式更多菜单，默认为空\",\n                    \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\n                                \"type\": \"string\",\n                                \"description\": \"菜单标识\"\n                            },\n                            \"title\": {\n                                \"type\": \"string\",\n                                \"description\": \"菜单标题\"\n                            }\n                        },\n                        \"required\": [\n                            \"name\",\n                            \"title\"\n                        ]\n                    }\n                },\n                \"preloadBase\": {\n                    \"type\": \"string\",\n                    \"description\": \"基础预加载文件，普通插件应用不需要设置\"\n                },\n                \"nodeIntegration\": {\n                    \"type\": \"boolean\",\n                    \"description\": \"是否启用 nodejs，普通应用不需要设置\"\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sdk/electron-browser-window.d.ts",
    "content": "declare module BrowserWindow {\n    interface WebPreferences {\n        devTools?: boolean;\n        preload?: string;\n        zoomFactor: number;\n        [key: string]: any;\n    }\n\n    interface InitOptions {\n        width?: number;\n        height?: number;\n        webPreferences: WebPreferences;\n        show?: boolean;\n        title?: string;\n        x?: number;\n        y?: number;\n        center?: boolean;\n        resizable?: boolean;\n        fullscreen?: boolean;\n        fullscreenable?: boolean;\n        skipTaskbar?: true;\n        closable?: boolean;\n        frame?: boolean;\n        alwayOnTop?: boolean;\n        [key: string]: any;\n    }\n\n    interface NativeImage {\n        toPng: (options?: {scaleFator?: number}) => Uint8Array;\n        toJPEG: (options?: {quality?: number}) => Uint8Array;\n        isEmpty: () => boolean;\n        [key: string]: any;\n    }\n\n    interface PrinterSync {\n        description: string;\n        displayName: string;\n        isDefault: boolean;\n        status: number;\n        options?: {\n            \"printer-location\"?: string;\n            \"printer-make-and-model\"?: string;\n            system_driverinfo?: string;\n        };\n    }\n\n    type WebRTCIPHandlingPolicy =\n        | \"default\"\n        | \"default_public_interface_only\"\n        | \"default_public_and_private_interfaces\"\n        | \"disable_non_proxied_udp\";\n\n    interface WebContents {\n        id: number;\n        capturePage: () => Promise<NativeImage>;\n        closeDevTools: () => void;\n        copy: () => void;\n        copyImageAt: (x: number, y: number) => void;\n        cut: () => void;\n        /**\n         * @deprecated\n         */\n        decrementCapturerCount: () => any;\n        delete: () => void;\n        disableDeviceEmulation: () => void;\n        enableDeviceEmulation: () => void;\n        executeJavaScript: <T>(code: string, userGesture?: boolean) => Promise<T>;\n        findInPage: (\n            text: string,\n            options?: {\n                forward?: boolean;\n                findNext?: boolean;\n                matchCase?: boolean;\n            }\n        ) => number;\n        focus: () => void;\n        getBackgroundThrottling: () => boolean;\n        getFrameRate: () => number;\n        getOSProcessId: () => number;\n        getPrinters: () => PrinterSync[];\n        getProcessId: () => number;\n        getUserAgent: () => string;\n        getWebRTCIPHandlingPolicy: () => WebRTCIPHandlingPolicy;\n        getZoomFactor: () => number;\n        /**\n         * @deprecated\n         */\n        incrementCapturerCount: () => any;\n        insertCSS: (\n            css: string,\n            options?: {\n                /**\n                 * @default 'author'\n                 */\n                cssOrigin?: \"user\" | \"author\";\n            }\n        ) => Promise<string>;\n        insertText: (text: string) => Promise<void>;\n        invalidate: () => void;\n        isAudioMuted: () => boolean;\n        isBeingCaptured: () => boolean;\n        isCrashed: () => boolean;\n        isCurrentlyAudible: () => boolean;\n        isDestroyed: () => boolean;\n        isDevToolsFocused: () => boolean;\n        isDevToolsOpened: () => boolean;\n        isFocused: () => boolean;\n        isLoading: () => boolean;\n        isLoadingMainFrame: () => boolean;\n        isOffscreen: () => boolean;\n        isPainting: () => void;\n        isWaitingForResponse: () => boolean;\n        openDevTools: (options?: {\n            mode: \"left\" | \"right\" | \"bottom\" | \"undocked\" | \"detach\";\n            activate?: boolean;\n            title?: string;\n        }) => void;\n        paste: () => void;\n        pasteAndMatchStyle: () => void;\n        print: (options?: Record<string, any>, callback?: (success: boolean, errorType?: string) => void) => void;\n        printToPDF: (options: Record<string, any>) => Promise<Uint8Array>;\n        redo: () => void;\n        removeInsertedCSS: (key: string) => Promise<void>;\n        replace: (text: string) => void;\n        replaceMisspelling: (text: string) => void;\n        savePage: (fullPath: string, saveType: \"HTMLOnly\" | \"HTMLComplete\" | \"MHTML\") => Promise<void>;\n        selectAll: () => void;\n        sendInputEvent: (e: any) => void;\n        setAudioMuted: (muted: boolean) => void;\n        setBackgroundThrottling: (allowed: boolean) => void;\n        setFrameRate: (fps: number) => void;\n        setIgnoreMenuShortcuts: (ignore: boolean) => void;\n        setUserAgent: (userAgent: string) => void;\n        setWebRTCIPHandlingPolicy: (policy: WebRTCIPHandlingPolicy) => void;\n        setZoomFactor: (factor: number) => void;\n        startPainting: () => void;\n        stopFindInPage: (action: \"clearSelection\" | \"keepSelection\" | \"activateSelection\") => void;\n        stopPainting: () => void;\n        takeHeapSnapshot: (filePath: string) => Promise<void>;\n        toggleDevTools: () => void;\n        undo: () => void;\n        unselect: () => void;\n\n        [key: string]: any;\n    }\n\n    interface Rectangle {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n    }\n\n    interface WindowInstance {\n        id: number;\n        webContents: WebContents;\n        show: () => void;\n        hide: () => void;\n        destory: () => void;\n        close: () => void;\n        isFocused: () => boolean;\n        isDestroyed: () => boolean;\n        setResizable: (resizable: boolean) => void;\n        setSize: (width: number, height: number) => void;\n        getSize: () => [width: number, height: number];\n        isVisible: () => boolean;\n        maximize: () => void;\n        unmaximize: () => void;\n        isMaximized: () => void;\n        minimize: () => void;\n        restore: () => void;\n        isMinimized: () => boolean;\n        setFullScreen: (flag: boolean) => void;\n        isFullScreen: () => boolean;\n        isNormal: () => boolean;\n        setAspectRatio: (aspectiRotio: number) => void;\n        setBackgroundColor: (backgroundColor: string) => void;\n        getBounds: () => Rectangle;\n        getBackgroundColor: () => string;\n        setContentBounds: (bounds: Rectangle) => void;\n        getContentBounds: () => Rectangle;\n        getNormalBounds: () => Rectangle;\n        setEnabled: (enable: boolean) => void;\n        isEnabled: () => boolean;\n        setContentSize: (width: number, height: number) => void;\n        getContentSize: () => [width: number, height: number];\n        setMinimumSize: (width: number, height: number) => void;\n        getMinimumSize: () => [width: number, height: number];\n        setMaximumSize: (width: number, height: number) => void;\n        getMaximumSize: () => [width: number, height: number];\n        isResizable: () => boolean;\n        setFullScreenable: (fullscreenable: boolean) => void;\n        isFullScreenable: () => boolean;\n        setClosable: (closable: boolean) => void;\n        isClosable: () => boolean;\n        setAlwaysOnTop: (flag: boolean) => void;\n        isAlwaysOnTop: () => boolean;\n        moveTop: () => void;\n        setPosition: (x: number, y: number) => void;\n        getPosition: () => [x: number, y: number];\n        setTitle: (title: string) => void;\n        getTitle: () => string;\n        flashFrame: (flag: boolean) => void;\n        setKiosk: (flag: boolean) => void;\n        isKiosk: () => boolean;\n        focusOnWebView: () => void;\n        blurWebView: () => void;\n        capturePage: (\n            rect?: Rectangle,\n            options?: {\n                stayHidden?: boolean;\n                stayAwake?: boolean;\n            }\n        ) => Promise<NativeImage>;\n        reload: () => void;\n\n        [key: string]: any;\n    }\n}\n"
  },
  {
    "path": "sdk/electron.d.ts",
    "content": "declare module \"electron\" {\n    type ClipboardType = \"selection\" | \"clipboard\";\n    module clipboard {\n        function availableFormats(type?: ClipboardType): void;\n\n        function clear(type?: ClipboardType): void;\n\n        function has(fmt: string, type?: ClipboardType): boolean;\n\n        function read(fmt: string): string;\n\n        function readBookmark(): {\n            title: string;\n            url: string;\n        };\n\n        function readBuffer(fmt: string): Uint8Array;\n\n        function readHTML(type?: ClipboardType): string;\n\n        function readImage(type?: ClipboardType): BrowserWindow.NativeImage;\n\n        function readRTF(type?: ClipboardType): string;\n\n        function readText(type?: ClipboardType): string;\n\n        function write(\n            data: {\n                text?: string;\n                html?: string;\n                image?: BrowserWindow.NativeImage;\n                rtf?: string;\n                bookmark?: string;\n            },\n            type?: ClipboardType\n        ): void;\n\n        function writeBookmark(title: string, url: string, type?: ClipboardType): void;\n\n        function writeBuffer(fmt: string, buffer: Uint8Array, type?: ClipboardType): void;\n\n        function writeHTML(markup: string, type?: ClipboardType): void;\n\n        function writeImage(img: BrowserWindow.NativeImage, type?: ClipboardType): void;\n\n        function writeRTF(text: string, type?: ClipboardType): void;\n\n        function writeText(text: string, type?: ClipboardType): void;\n    }\n\n    interface UIpcSendEventInit {\n        senderId: number;\n    }\n\n    type UIpcSendEventListener<T extends any[]> = (event: UIpcSendEventInit, ...args: T) => void;\n    module ipcRenderer {\n        function on<T extends any[] = any[]>(channel: string, listener: UIpcSendEventListener<T>): void;\n\n        function once<T extends any[] = any[]>(channel: string, listener: UIpcSendEventListener<T>): void;\n\n        function off<T extends any[] = any[]>(channel: string, listener: UIpcSendEventListener<T>): void;\n\n        function sendTo<T extends any[] = any[]>(id: number, channel: string, ...args: T): void;\n    }\n\n    module contextBridge {}\n\n    module webFrame {}\n\n    module shell {}\n\n    module nativeImage {\n        type NativeImage = BrowserWindow.NativeImage;\n\n        function createEmpty(): NativeImage;\n\n        function createFromPath(path: string): NativeImage;\n\n        function createFromBitmap(\n            buffer: Uint8Array,\n            options: {\n                width: number;\n                height: number;\n                scaleFator?: number;\n            }\n        ): NativeImage;\n\n        function createFromBuffer(\n            buffer: Uint8Array,\n            options?: {\n                width?: number;\n                height?: number;\n                scaleFator?: number;\n            }\n        ): NativeImage;\n\n        function createFromDataURL(dataURL: string): NativeImage;\n    }\n}\n"
  },
  {
    "path": "sdk/focusany-shim.d.ts",
    "content": "export interface FocusAnyShimType {\n    init(): void;\n}\n\nexport declare const FocusAnyShim: FocusAnyShimType;\n"
  },
  {
    "path": "sdk/focusany-shim.ts",
    "content": "/// <reference path=\"focusany.d.ts\" />\n\nconst FocusAnyShim = {\n    init() {\n        if (window[\"focusany\"]) {\n            return;\n        }\n\n        let hooks: Record<string, any> = {\n            onLog: (label: string, data?: any) => {\n                console.log(`FocusAny Log: ${label}`, data || \"\");\n            }\n        }\n\n        const focusanySupport = {\n            onLog(callback: (label: string, data?: any) => void): void {\n                hooks.onLog = callback;\n            },\n            onPluginReady(\n                callback: (data: {\n                    actionName: string;\n                    actionMatch: any | null;\n                    actionMatchFiles: FileItem[];\n                    requestId: string;\n                    reenter: boolean;\n                    isView: boolean;\n                }) => void\n            ): void {\n                const callbackWrapper = () => {\n                    callback({\n                        actionName: \"\",\n                        actionMatch: null,\n                        actionMatchFiles: [],\n                        requestId: \"\",\n                        reenter: false,\n                        isView: false,\n                    });\n                };\n                if (document.readyState === \"loading\") {\n                    document.addEventListener(\"DOMContentLoaded\", callbackWrapper);\n                } else {\n                    callbackWrapper();\n                }\n            },\n            copyText(text: string): boolean {\n                if (navigator.clipboard && navigator.clipboard.writeText) {\n                    navigator.clipboard.writeText(text);\n                    return true;\n                } else {\n                    console.error(\"FocusAny Shim: copyText() requires clipboard permission in web environment\");\n                    return false;\n                }\n            },\n            isMacOs(): boolean {\n                return navigator.platform.toLowerCase().includes(\"mac\");\n            },\n            isWindows(): boolean {\n                return navigator.platform.toLowerCase().includes(\"win\");\n            },\n            isLinux(): boolean {\n                return navigator.platform.toLowerCase().includes(\"linux\");\n            },\n            showNotification(body: string, clickActionName?: string): void {\n                focusanySupport.showToast(body, {\n                    duration: 5000,\n                    status: \"info\",\n                });\n            },\n            showToast(body: string, options?: { duration?: number; status?: \"info\" | \"success\" | \"error\" }): void {\n                options = options || {};\n                const duration =\n                    typeof options.duration === \"number\" && options.duration >= 0 ? options.duration : 3000;\n                const status = [\"info\", \"success\", \"error\"].includes(options.status as string) ? options.status : \"info\";\n\n                // 创建SVG图标函数\n                const createSvgIcon = (type: string): string => {\n                    const svgBase =\n                        'xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\"';\n\n                    switch (type) {\n                        case \"info\":\n                            return `<svg ${svgBase}><circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"rgba(255,255,255,0.15)\" stroke=\"currentColor\" stroke-width=\"1\"/><path d=\"M8 4a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3A.5.5 0 0 1 8 4zM8 11a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5z\"/></svg>`;\n                        case \"success\":\n                            return `<svg ${svgBase}><circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"rgba(255,255,255,0.15)\" stroke=\"currentColor\" stroke-width=\"1\"/><path d=\"M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z\"/></svg>`;\n                        case \"error\":\n                            return `<svg ${svgBase}><circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"rgba(255,255,255,0.15)\" stroke=\"currentColor\" stroke-width=\"1\"/><path d=\"M5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z\"/></svg>`;\n                        default:\n                            return `<svg ${svgBase}><circle cx=\"8\" cy=\"8\" r=\"6\" fill=\"rgba(255,255,255,0.15)\"/></svg>`;\n                    }\n                };\n\n                const statusStyles = {\n                    info: {background: \"#1890ff\", color: \"#ffffff\", icon: createSvgIcon(\"info\")},\n                    success: {background: \"#52c41a\", color: \"#ffffff\", icon: createSvgIcon(\"success\")},\n                    error: {background: \"#ff4d4f\", color: \"#ffffff\", icon: createSvgIcon(\"error\")},\n                };\n                const currentStyle = statusStyles[status as keyof typeof statusStyles];\n\n                let container = document.getElementById(\"focusany-shim-toast-container\");\n                if (!container) {\n                    container = document.createElement(\"div\");\n                    container.id = \"focusany-shim-toast-container\";\n                    container.style.cssText = `\n                        position: fixed !important;\n                        top: 20px !important;\n                        right: 20px !important;\n                        z-index: 999999 !important;\n                        font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif !important;\n                        pointer-events: none !important;\n                        max-width: 350px !important;\n                        width: auto !important;\n                    `;\n                    document.body.appendChild(container);\n                }\n\n                // 创建通知元素\n                const notification = document.createElement(\"div\");\n                notification.style.cssText = `\n                    background: ${currentStyle.background} !important;\n                    color: ${currentStyle.color} !important;\n                    padding: 12px 30px 12px 16px !important;\n                    margin-bottom: 10px !important;\n                    border-radius: 6px !important;\n                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;\n                    font-size: 14px !important;\n                    line-height: 1.4 !important;\n                    width: 100% !important;\n                    max-width: 320px !important;\n                    word-wrap: break-word !important;\n                    opacity: 0 !important;\n                    transform: translateX(350px) !important;\n                    transition: all 0.3s ease !important;\n                    pointer-events: auto !important;\n                    cursor:default !important;\n                    border: none !important;\n                    outline: none !important;\n                    text-decoration: none !important;\n                    box-sizing: border-box !important;\n                    display: block !important;\n                    position: relative !important;\n                    min-height: 20px !important;\n                `;\n\n                // 创建内容容器\n                const content = document.createElement(\"div\");\n                content.style.cssText = `\n                    display: flex !important;\n                    align-items: center !important;\n                    gap: 8px !important;\n                `;\n\n                // 添加状态图标\n                const iconSpan = document.createElement(\"span\");\n                iconSpan.innerHTML = currentStyle.icon;\n                iconSpan.style.cssText = `\n                    font-size: 16px !important;\n                    line-height: 1 !important;\n                    flex-shrink: 0 !important;\n                    padding: 4px 6px !important;\n                    border-radius: 4px !important;\n                    display: inline-flex !important;\n                    align-items: center !important;\n                    justify-content: center !important;\n                `;\n\n                // 添加文本内容\n                const textSpan = document.createElement(\"span\");\n                textSpan.textContent = body;\n                textSpan.style.cssText = `\n                    flex: 1 !important;\n                `;\n\n                content.appendChild(iconSpan);\n                content.appendChild(textSpan);\n                notification.appendChild(content);\n                const closeButton = document.createElement(\"span\");\n                closeButton.textContent = \"×\";\n                closeButton.style.cssText = `\n                    position: absolute !important;\n                    top: 50% !important;\n                    right: 8px !important;\n                    transform: translateY(-50%) !important;\n                    font-size: 16px !important;\n                    font-weight: bold !important;\n                    cursor: pointer !important;\n                    color: rgba(255, 255, 255, 0.8) !important;\n                    line-height: 1 !important;\n                    width: 20px !important;\n                    height: 20px !important;\n                    text-align: center !important;\n                    display: flex !important;\n                    align-items: center !important;\n                    justify-content: center !important;\n                    border-radius: 50% !important;\n                    background: rgba(255, 255, 255, 0.1) !important;\n                    transition: all 0.2s ease !important;\n                    backdrop-filter: blur(4px) !important;\n                `;\n                closeButton.addEventListener(\"mouseenter\", () => {\n                    closeButton.style.color = \"#ffffff !important\";\n                    closeButton.style.backgroundColor = \"rgba(255, 255, 255, 0.2) !important\";\n                    closeButton.style.transform = \"translateY(-50%) scale(1.1) !important\";\n                });\n                closeButton.addEventListener(\"mouseleave\", () => {\n                    closeButton.style.color = \"rgba(255, 255, 255, 0.8) !important\";\n                    closeButton.style.backgroundColor = \"rgba(255, 255, 255, 0.1) !important\";\n                    closeButton.style.transform = \"translateY(-50%) scale(1) !important\";\n                });\n                closeButton.addEventListener(\"click\", e => {\n                    e.stopPropagation();\n                    removeNotification();\n                });\n                notification.appendChild(closeButton);\n                container.appendChild(notification);\n                setTimeout(() => {\n                    notification.style.setProperty(\"opacity\", \"1\", \"important\");\n                    notification.style.setProperty(\"transform\", \"translateX(0)\", \"important\");\n                }, 10);\n                const removeNotification = () => {\n                    notification.style.setProperty(\"opacity\", \"0\", \"important\");\n                    notification.style.setProperty(\"transform\", \"translateX(350px)\", \"important\");\n                    setTimeout(() => {\n                        if (notification.parentNode) {\n                            notification.parentNode.removeChild(notification);\n                        }\n                        // 如果容器为空，移除容器\n                        if (container && container.children.length === 0) {\n                            if (container.parentNode) {\n                                container.parentNode.removeChild(container);\n                            }\n                        }\n                    }, 300);\n                };\n\n                // 使用配置的 duration 时间自动移除\n                if (duration > 0) {\n                    setTimeout(removeNotification, duration);\n                }\n            },\n            // use localStorage with prefix db:xxx to store data\n            db: {\n                put(doc: DbDoc): DbReturn {\n                    const key = `db:${doc._id}`;\n                    try {\n                        localStorage.setItem(key, JSON.stringify(doc));\n                        return {\n                            ok: true,\n                            id: doc._id,\n                            rev: doc._rev || focusanySupport.util.randomString(16),\n                        };\n                    } catch (e) {\n                        console.error(\"FocusAny Shim: db.put() failed:\", e);\n                        return {ok: false, id: doc._id, rev: \"\"};\n                    }\n                },\n                get<T extends {} = Record<string, any>>(id: string): DbDoc<T> | null {\n                    const key = `db:${id}`;\n                    const value = localStorage.getItem(key);\n                    if (value) {\n                        try {\n                            return JSON.parse(value) as DbDoc<T>;\n                        } catch (e) {\n                            console.error(\"FocusAny Shim: db.get() failed:\", e);\n                            return null;\n                        }\n                    }\n                    return null;\n                },\n                remove(doc: string | DbDoc): DbReturn {\n                    const id = typeof doc === \"string\" ? doc : doc._id;\n                    const key = `db:${id}`;\n                    try {\n                        localStorage.removeItem(key);\n                        return {ok: true, id, rev: \"\"};\n                    } catch (e) {\n                        console.error(\"FocusAny Shim: db.remove() failed:\", e);\n                        return {ok: false, id, rev: \"\"};\n                    }\n                },\n                bulkDocs(docs: DbDoc[]): DbReturn[] {\n                    const results: DbReturn[] = [];\n                    for (const doc of docs) {\n                        const result = this.put(doc);\n                        results.push(result);\n                    }\n                    return results;\n                },\n                allDocs<T extends {} = Record<string, any>>(key?: string): DbDoc<T>[] {\n                    const results: DbDoc<T>[] = [];\n                    const prefix = key ? `db:${key}` : \"db:\";\n                    for (const item of Object.keys(localStorage)) {\n                        if (item.startsWith(prefix)) {\n                            const value = localStorage.getItem(item);\n                            if (value) {\n                                try {\n                                    results.push(JSON.parse(value) as DbDoc<T>);\n                                } catch (e) {\n                                    console.error(\"FocusAny Shim: db.allDocs() failed:\", e);\n                                }\n                            }\n                        }\n                    }\n                    return results;\n                },\n                postAttachment(docId: string, attachment: Uint8Array, type: string): DbReturn {\n                    const key = `dbAttachment:${docId}`;\n                    try {\n                        const existing = localStorage.getItem(key);\n                        const attachments = existing ? JSON.parse(existing) : {};\n                        attachments[type] = focusanySupport.util.bufferToBase64(attachment);\n                        localStorage.setItem(key, JSON.stringify(attachments));\n                        return {ok: true, id: docId, rev: \"\"};\n                    } catch (e) {\n                        console.error(\"FocusAny Shim: db.postAttachment() failed:\", e);\n                        return {ok: false, id: docId, rev: \"\"};\n                    }\n                },\n                getAttachment(docId: string): Uint8Array | null {\n                    const key = `dbAttachment:${docId}`;\n                    const value = localStorage.getItem(key);\n                    if (value) {\n                        try {\n                            const attachments = JSON.parse(value);\n                            const firstKey = Object.keys(attachments)[0];\n                            if (firstKey) {\n                                return focusanySupport.util.base64ToBuffer(attachments[firstKey]);\n                            }\n                        } catch (e) {\n                            console.error(\"FocusAny Shim: db.getAttachment() failed:\", e);\n                        }\n                    }\n                    return null;\n                },\n                getAttachmentType(docId: string): string | null {\n                    const key = `dbAttachment:${docId}`;\n                    const value = localStorage.getItem(key);\n                    if (value) {\n                        try {\n                            const attachments = JSON.parse(value);\n                            const firstKey = Object.keys(attachments)[0];\n                            return firstKey || null;\n                        } catch (e) {\n                            console.error(\"FocusAny Shim: db.getAttachmentType() failed:\", e);\n                        }\n                    }\n                    return null;\n                },\n            },\n            dbStorage: {\n                setItem(key: string, value: any): void {\n                    localStorage.setItem(key, JSON.stringify(value));\n                },\n                getItem<T = any>(key: string): T {\n                    const value = localStorage.getItem(key);\n                    return value ? JSON.parse(value) : null as any;\n                },\n                removeItem(key: string): void {\n                    localStorage.removeItem(key);\n                },\n            },\n            util: {\n                randomString(length: number): string {\n                    const chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n                    let result = \"\";\n                    for (let i = 0; i < length; i++) {\n                        result += chars.charAt(Math.floor(Math.random() * chars.length));\n                    }\n                    return result;\n                },\n                bufferToBase64(buffer: Uint8Array): string {\n                    return btoa(String.fromCharCode.apply(null, Array.from(buffer)));\n                },\n                base64ToBuffer(base64: string): Uint8Array {\n                    const binary = atob(base64);\n                    const bytes = new Uint8Array(binary.length);\n                    for (let i = 0; i < binary.length; i++) {\n                        bytes[i] = binary.charCodeAt(i);\n                    }\n                    return bytes;\n                },\n                datetimeString(): string {\n                    return new Date().toISOString();\n                },\n                base64Encode(data: any): string {\n                    return btoa(JSON.stringify(data));\n                },\n                base64Decode(data: string): any {\n                    return JSON.parse(atob(data));\n                },\n                md5(data: string): string {\n                    console.error(\n                        \"FocusAny Shim: util.md5() is not supported in web environment, use crypto.subtle instead\"\n                    );\n                    return \"\";\n                },\n                save(filename: string, data: string | Uint8Array, option?: { isBase64?: boolean }): boolean {\n                    // 使用浏览器下载功能\n                    try {\n                        const blob = new Blob([data as any], {type: \"application/octet-stream\"});\n                        const url = URL.createObjectURL(blob);\n                        const a = document.createElement(\"a\");\n                        a.href = url;\n                        a.download = filename;\n                        document.body.appendChild(a);\n                        a.click();\n                        document.body.removeChild(a);\n                        URL.revokeObjectURL(url);\n                        return true;\n                    } catch (e) {\n                        console.error(\"FocusAny Shim: util.save() failed:\", e);\n                        return false;\n                    }\n                },\n            },\n            // Additional unimplemented methods with mock data\n            onPluginExit(callback: Function): void {\n                // Mock: do nothing\n            },\n            onPluginEvent(event: PluginEvent, callback: (data: any) => void): void {\n                // Mock: do nothing\n            },\n            offPluginEvent(event: PluginEvent, callback: (data: any) => void): void {\n                // Mock: do nothing\n            },\n            offPluginEventAll(event: PluginEvent): void {\n                // Mock: do nothing\n            },\n            onMoreMenuClick(callback: (data: { name: string }) => void): void {\n                // Mock: do nothing\n            },\n            registerHotkey(key: string | string[] | HotkeyQuickType | HotkeyType | HotkeyType[], callback: () => void): void {\n                // Mock: do nothing\n            },\n            unregisterHotkeyAll(): void {\n                // Mock: do nothing\n            },\n            isMainWindowShown(): boolean {\n                return true; // Mock: always shown\n            },\n            hideMainWindow(): void {\n                // Mock: do nothing\n            },\n            showMainWindow(): void {\n                // Mock: do nothing\n            },\n            isFastPanelWindowShown(): boolean {\n                return false; // Mock: not shown\n            },\n            showFastPanelWindow(): void {\n                // Mock: do nothing\n            },\n            hideFastPanelWindow(): void {\n                // Mock: do nothing\n            },\n            setExpendHeight(height: number): void {\n                // Mock: do nothing\n            },\n            setSubInput(onChange: (keywords: string) => void, placeholder?: string, isFocus?: boolean, isVisible?: boolean): void {\n                // Mock: do nothing\n            },\n            removeSubInput(): void {\n                // Mock: do nothing\n            },\n            setSubInputValue(value: string): void {\n                // Mock: do nothing\n            },\n            subInputBlur(): void {\n                // Mock: do nothing\n            },\n            getPluginRoot(): string {\n                return \"/mock/plugin/root\"; // Mock path\n            },\n            getPluginConfig(): { name: string; title: string; version: string; logo: string; } | null {\n                return {\n                    name: \"mock-plugin\",\n                    title: \"Mock Plugin\",\n                    version: \"1.0.0\",\n                    logo: \"\"\n                };\n            },\n            getPluginInfo(): {\n                nodeIntegration: boolean;\n                preloadBase: string;\n                preload: string;\n                main: string;\n                mainView: string;\n                width: number;\n                height: number;\n                autoDetach: boolean;\n                singleton: boolean;\n                zoom: number;\n            } {\n                return {\n                    nodeIntegration: false,\n                    preloadBase: \"\",\n                    preload: \"\",\n                    main: \"\",\n                    mainView: \"\",\n                    width: 800,\n                    height: 600,\n                    autoDetach: false,\n                    singleton: false,\n                    zoom: 1\n                };\n            },\n            getPluginEnv(): \"dev\" | \"prod\" {\n                return \"dev\";\n            },\n            getQuery(requestId: string): SearchQuery {\n                return {\n                    keywords: \"\",\n                    currentFiles: [],\n                    currentImage: \"\",\n                    currentText: \"\"\n                };\n            },\n            createBrowserWindow(url: string, options: any, callback?: () => void): any {\n                // Mock: open in new tab\n                window.open(url, \"_blank\");\n                if (callback) callback();\n                return {\n                    close: () => {\n                    }\n                };\n            },\n            outPlugin(): void {\n                // Mock: do nothing\n            },\n            isDarkColors(): boolean {\n                return window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n            },\n            showUserLogin(): void {\n                // Mock: do nothing\n            },\n            getUser(): {\n                isLogin: boolean;\n                avatar: string;\n                nickname: string;\n                vipFlag: string;\n                deviceCode: string;\n                openId: string;\n            } {\n                return {\n                    isLogin: false,\n                    avatar: \"\",\n                    nickname: \"Mock User\",\n                    vipFlag: \"\",\n                    deviceCode: \"mock-device\",\n                    openId: \"\"\n                };\n            },\n            getUserAccessToken(): Promise<{ token: string; expireAt: number; }> {\n                return Promise.resolve({\n                    token: \"mock-token\",\n                    expireAt: Date.now() + 3600000\n                });\n            },\n            listGoods(query?: { ids?: string[] }): Promise<{\n                id: string;\n                title: string;\n                cover: string;\n                priceType: \"fixed\" | \"dynamic\";\n                fixedPrice: string;\n                description: string;\n            }[]> {\n                return Promise.resolve([]);\n            },\n            openGoodsPayment(options: {\n                goodsId: string;\n                price?: string;\n                outOrderId?: string;\n                outParam?: string;\n            }): Promise<{ paySuccess: boolean; }> {\n                return Promise.resolve({paySuccess: false});\n            },\n            queryGoodsOrders(options: { goodsId?: string; page?: number; pageSize?: number; }): Promise<{\n                page: number;\n                total: number;\n                records: { id: string; goodsId: string; status: \"Paid\" | \"Unpaid\"; }[];\n            }> {\n                return Promise.resolve({\n                    page: 1,\n                    total: 0,\n                    records: []\n                });\n            },\n            apiPost(url: string, body: any, option: {}): Promise<BaseResult> {\n                return Promise.resolve({\n                    code: 200,\n                    msg: \"Mock response\",\n                    data: null\n                });\n            },\n            setAction(action: PluginAction | PluginAction[]): void {\n                // Mock: do nothing\n            },\n            removeAction(name: string): void {\n                // Mock: do nothing\n            },\n            getActions(names?: string[]): PluginAction[] {\n                return [];\n            },\n            redirect(keywordsOrAction: string | string[], query?: SearchQuery): void {\n                // Mock: do nothing\n            },\n            showMessageBox(message: string, options: { title?: string; yes?: string; no?: string; }): boolean {\n                return confirm(message); // Mock: use browser confirm\n            },\n            showOpenDialog(options: {\n                title?: string;\n                defaultPath?: string;\n                buttonLabel?: string;\n                filters?: { name: string; extensions: string[] }[];\n                properties?: any[];\n                message?: string;\n                securityScopedBookmarks?: boolean;\n            }): string[] | undefined {\n                // Mock: return empty array\n                return [];\n            },\n            showSaveDialog(options: {\n                title?: string;\n                defaultPath?: string;\n                buttonLabel?: string;\n                filters?: { name: string; extensions: string[] }[];\n                message?: string;\n                nameFieldLabel?: string;\n                showsTagField?: string;\n                properties?: any[];\n                securityScopedBookmarks?: boolean;\n            }): string | undefined {\n                // Mock: return null\n                return undefined;\n            },\n            screenCapture(callback: (imgBase64: string) => void): void {\n                // Mock: do nothing\n            },\n            getNativeId(): string {\n                return \"mock-native-id\";\n            },\n            getAppVersion(): string {\n                return \"1.0.0\";\n            },\n            getPath(name: \"home\" | \"appData\" | \"userData\" | \"temp\" | \"exe\" | \"desktop\" | \"documents\" | \"downloads\" | \"music\" | \"pictures\" | \"videos\" | \"logs\"): string {\n                return `/mock/${name}`;\n            },\n            getFileIcon(path: string): string {\n                return \"\"; // Mock: empty icon\n            },\n            copyFile(file: string | string[]): boolean {\n                return false; // Mock: not supported\n            },\n            copyImage(image: string): boolean {\n                return false; // Mock: not supported\n            },\n            getClipboardText(): string {\n                return \"\"; // Mock: empty\n            },\n            getClipboardImage(): string {\n                return \"\"; // Mock: empty\n            },\n            getClipboardFiles(): FileItem[] {\n                return [];\n            },\n            listClipboardItems(option?: { limit?: number }): Promise<{\n                type: \"file\" | \"image\" | \"text\";\n                timestamp: number;\n                files?: FileItem[];\n                image?: string;\n                text?: string;\n            }[]> {\n                return Promise.resolve([]);\n            },\n            deleteClipboardItem(timestamp: number): Promise<void> {\n                return Promise.resolve();\n            },\n            clearClipboardItems(): Promise<void> {\n                return Promise.resolve();\n            },\n            shellOpenPath(fullPath: string): void {\n                // Mock: do nothing\n            },\n            shellShowItemInFolder(fullPath: string): void {\n                // Mock: do nothing\n            },\n            shellOpenExternal(url: string): void {\n                window.open(url, \"_blank\");\n            },\n            shellBeep(): void {\n                // Mock: do nothing\n            },\n            simulate: {\n                keyboardTap(key: string, modifiers: (\"ctrl\" | \"shift\" | \"command\" | \"option\" | \"alt\")[]): Promise<void> {\n                    return Promise.resolve();\n                },\n                typeString(text: string): Promise<void> {\n                    return Promise.resolve();\n                },\n                mouseToggle(type: \"down\" | \"up\", button: \"left\" | \"right\" | \"middle\"): Promise<void> {\n                    return Promise.resolve();\n                },\n                mouseMove(x: number, y: number): Promise<void> {\n                    return Promise.resolve();\n                },\n                mouseClick(button: \"left\" | \"right\" | \"middle\", double?: boolean): Promise<void> {\n                    return Promise.resolve();\n                },\n            },\n            getCursorScreenPoint(): { x: number; y: number } {\n                return {x: 0, y: 0}; // Mock: center\n            },\n            getDisplayNearestPoint(point: { x: number; y: number }): any {\n                return {\n                    id: 1,\n                    bounds: {x: 0, y: 0, width: 1920, height: 1080},\n                    workArea: {x: 0, y: 0, width: 1920, height: 1040},\n                    scaleFactor: 1\n                };\n            },\n            getPlatformArch(): \"x86\" | \"arm64\" | null {\n                return \"x86\"; // Mock\n            },\n            sendBackendEvent(event: string, data?: any, option?: { timeout: number; }): Promise<any> {\n                return Promise.resolve(null);\n            },\n            registerCallPage(type: string, callback: (resolve: (data: any) => void, reject: (error: string) => void, data: any) => void, option?: {\n                timeout?: number;\n            }): void {\n                // Mock: do nothing\n            },\n            callPage(type: string, data?: any, option?: CallPageOption): Promise<any> {\n                return Promise.resolve(null);\n            },\n            setRemoteWebRuntime(info: {\n                userAgent: string;\n                urlMap: Record<string, string>;\n                types: string[];\n                domains: string[];\n                blocks: string[];\n            }): Promise<undefined> {\n                return Promise.resolve(undefined);\n            },\n            llmListModels(): Promise<{\n                providerId: string;\n                providerLogo: string;\n                providerTitle: string;\n                modelId: string;\n                modelName: string;\n            }[]> {\n                return Promise.resolve([\n                    {\n                        providerId: \"openai\",\n                        providerLogo: \"https://cdn.openai.com/chatgpt/images/chatgpt-logo.png\",\n                        providerTitle: \"OpenAI\",\n                        modelId: \"gpt-4\",\n                        modelName: \"GPT-4\"\n                    },\n                    {\n                        providerId: \"anthropic\",\n                        providerLogo: \"https://www.anthropic.com/images/anthropic-logo.png\",\n                        providerTitle: \"Anthropic\",\n                        modelId: \"claude-3-opus\",\n                        modelName: \"Claude 3 Opus\"\n                    },\n                ]);\n            },\n            llmChat(callInfo: { providerId: string; modelId: string; message: string }): Promise<BaseResult<{\n                message: string;\n            }>> {\n                return Promise.resolve({\n                    code: 200,\n                    msg: \"Mock response\",\n                    data: {message: \"Mock LLM response\"}\n                });\n            },\n            logInfo(label: string, data?: any): void {\n                console.log(`FocusAny Log Info: ${label}`, data || \"\");\n            },\n            logError(label: string, data?: any): void {\n                console.error(`FocusAny Log Error: ${label}`, data || \"\");\n            },\n            logPath(): Promise<string> {\n                return Promise.resolve(\"/mock/log/path\");\n            },\n            logShow(): void {\n                // Mock: do nothing\n            },\n            addLaunch(keyword: string, name: string, hotkey: HotkeyType): Promise<void> {\n                return Promise.resolve();\n            },\n            removeLaunch(keyword: string): void {\n                // Mock: do nothing\n            },\n            activateLatestWindow(): Promise<void> {\n                return Promise.resolve();\n            },\n            file: {\n                exists(path: string): Promise<boolean> {\n                    return Promise.resolve(false);\n                },\n                read(path: string): Promise<string> {\n                    return Promise.resolve(\"\");\n                },\n                write(path: string, data: string): Promise<void> {\n                    return Promise.resolve();\n                },\n                remove(path: string): Promise<void> {\n                    return Promise.resolve();\n                },\n                ext(path: string): Promise<string> {\n                    return Promise.resolve(\"\");\n                },\n                writeTemp(ext: string, data: string | Uint8Array, option?: { isBase64?: boolean; }): Promise<string> {\n                    return Promise.resolve(`/mock/temp/file.${ext}`);\n                },\n            },\n            fad: {\n                read(type: string, path: string): Promise<any> {\n                    return Promise.resolve(null);\n                },\n                write(type: string, path: string, data: any): Promise<void> {\n                    return Promise.resolve();\n                },\n            },\n            view: {\n                setHeight(height: number): void {\n                    // Mock: do nothing\n                },\n                getHeight(): Promise<number> {\n                    return Promise.resolve(400);\n                },\n            },\n            detach: {\n                setTitle(title: string): void {\n                    // Mock: do nothing\n                },\n                setOperates(operates: { name: string; title: string; click: () => void; }[]): void {\n                    // Mock: do nothing\n                },\n                setPosition(position: \"center\" | \"right-bottom\" | \"left-top\" | \"right-top\" | \"left-bottom\"): void {\n                    // Mock: do nothing\n                },\n                setAlwaysOnTop(alwaysOnTop: boolean): void {\n                    // Mock: do nothing\n                },\n                setSize(width: number, height: number): void {\n                    // Mock: do nothing\n                },\n            },\n        } as FocusAnyApi\n\n        // 创建一个递归的 Proxy 来处理任意深度的属性访问\n        function createErrorProxy(path: string = \"focusany\", supportObj?: any): any {\n            return new Proxy(() => {\n            }, {\n                get(target, prop) {\n                    const currentPath = `${path}.${String(prop)}`;\n                    // 如果是根级别且在支持对象中存在，直接返回\n                    if (path === \"focusany\" && supportObj && prop in supportObj) {\n                        const result = supportObj[prop];\n                        // console.log('FocusAny Shim: Accessing supported property:', {currentPath, result});\n                        return new Proxy(result, {\n                            get(t, p) {\n                                // console.log('FocusAny Shim: Accessing supported sub-property:', {currentPath: `${currentPath}.${String(p)}`});\n                                const value = (t as any)[p];\n                                if (typeof value === \"function\") {\n                                    return function (...args: any[]) {\n                                        return value.apply(t, args);\n                                    };\n                                }\n                                return value;\n                            },\n                            apply(t, thisArg, args) {\n                                console.log('FocusAny Shim: Calling supported function:', {\n                                    t,\n                                    currentPath,\n                                    thisArg,\n                                    args\n                                });\n                                const ret = (t as any).apply(thisArg, args);\n                                const pcs = currentPath.split(\".\");\n                                const name = pcs[pcs.length - 1];\n                                hooks.onLog && hooks.onLog(name, args.map(a => {\n                                    if (a instanceof Function) return 'function(){}';\n                                    return a;\n                                }));\n                                if (ret instanceof Promise) {\n                                    return ret.then((data: any) => {\n                                        hooks.onLog && hooks.onLog(`${name}.result`, data);\n                                        return data;\n                                    });\n                                } else {\n                                    hooks.onLog && hooks.onLog(`${name}.result`, ret);\n                                }\n                                return ret;\n                            }\n                        });\n                    }\n                    // 对于其他属性，返回一个新的 Proxy（不支持任何属性）\n                    return createErrorProxy(currentPath, null);\n                },\n                apply(target, thisArg, argumentsList) {\n                    console.error(`FocusAny Shim: ${path}() is not supported in web environment`);\n                },\n            });\n        }\n\n        // 创建 focusany 对象\n        const focusany = createErrorProxy(\"focusany\", focusanySupport);\n        // @ts-ignore\n        window[\"focusany\"] = focusany;\n    },\n};\n\n// 自动初始化：在浏览器环境中自动调用 init()\nif (typeof window !== \"undefined\") {\n    FocusAnyShim.init();\n}\n"
  },
  {
    "path": "sdk/focusany.d.ts",
    "content": "/// <reference path=\"electron-browser-window.d.ts\"/>\n/// <reference path=\"electron.d.ts\"/>\n\ndeclare interface Window {\n    focusany: FocusAnyApi;\n}\n\ntype DbDoc<T extends {} = Record<string, any>> = {\n    _id: string;\n    _rev?: string;\n} & T;\n\ninterface DbReturn {\n    id: string;\n    rev?: string;\n    ok?: boolean;\n    error?: boolean;\n    name?: string;\n    message?: string;\n}\n\ndeclare type BaseResult<T = any> = {\n    code: number;\n    msg: string;\n    data?: T;\n};\n\ndeclare type PlatformType = \"win\" | \"osx\" | \"linux\";\n\ndeclare type EditionType = \"open\" | \"pro\";\n\ndeclare type PluginEvent = \"ClipboardChange\" | \"UserChange\";\n\ndeclare type HotkeyModifierType = \"Control\" | \"Option\" | \"Command\" | \"Ctrl\" | \"Alt\" | \"Win\" | \"Meta\" | \"Shift\";\n\ndeclare type HotkeyType = { key: string; modifiers: HotkeyModifierType[] };\n\ndeclare type HotkeyQuickType = \"save\";\n\ndeclare type ActionMatch =\n    | ActionMatchText\n    | ActionMatchKey\n    | ActionMatchRegex\n    | ActionMatchFile\n    | ActionMatchImage\n    | ActionMatchWindow\n    | ActionMatchEditor;\n\ndeclare enum ActionMatchTypeEnum {\n    TEXT = \"text\",\n    KEY = \"key\",\n    REGEX = \"regex\",\n    IMAGE = \"image\",\n    FILE = \"file\",\n    WINDOW = \"window\",\n    EDITOR = \"editor\",\n}\n\ntype SearchQuery = {\n    keywords: string;\n    currentFiles?: FileItem[];\n    currentImage?: string;\n    currentText?: string;\n};\n\ntype FileItem = {\n    name: string;\n    isDirectory: boolean;\n    isFile: boolean;\n    path: string;\n    fileExt: string;\n};\n\ntype ActionCodeSetting = {\n    type: \"list\",\n    placeholder: string;\n}\n\ntype ActionCodeExecuteResultItem = {\n    id: string;\n    icon: string;\n    title: string;\n    description: string;\n    loading?: boolean;\n    // additional data\n    [key: string]: any;\n}\n\ntype ActionCodeExecuteResult = {\n    command: \"data\" | \"none\" | \"error\" | \"close\" | \"clear\";\n    // set placeholder when placeholder is set\n    placeholder?: string;\n    // command === data\n    items?: ActionCodeExecuteResultItem[],\n    // command === error\n    error?: string;\n    // additional data\n    [key: string]: any;\n}\n\ndeclare type ActionMatchBase = {\n    type: ActionMatchTypeEnum;\n    name?: string;\n};\n\ndeclare type ActionMatchText = ActionMatchBase & {\n    text: string;\n    minLength: number;\n    maxLength: number;\n};\n\ndeclare type ActionMatchKey = ActionMatchBase & {\n    key: string;\n};\n\ndeclare type ActionMatchRegex = ActionMatchBase & {\n    regex: string;\n    title: string;\n    minLength: number;\n    maxLength: number;\n};\n\ndeclare type ActionMatchFile = ActionMatchBase & {\n    title: string;\n    minCount: number;\n    maxCount: number;\n    filterFileType: \"file\" | \"directory\";\n    filterExtensions: string[];\n};\n\ndeclare type ActionMatchImage = ActionMatchBase & {\n    title: string;\n};\n\ndeclare type ActionMatchWindow = ActionMatchBase & {\n    nameRegex: string;\n    titleRegex: string;\n    attrRegex: Record<string, string>;\n};\n\ndeclare type ActionMatchEditor = ActionMatchBase & {\n    extensions: string[];\n    fadTypes: string[];\n};\n\ninterface PluginAction {\n    fullName?: string;\n    name: string;\n    title: string;\n    matches: ActionMatch[];\n    platform?: PlatformType[];\n    icon?: string;\n    type?: \"command\" | \"web\" | \"code\" | \"backend\";\n}\n\ndeclare type CallPageOption = {\n    // default 10000 ms\n    waitReadyTimeout?: number,\n    // default 60000 ms\n    timeout?: number;\n    // default true, if false the render function will not work\n    showWindow?: boolean;\n    // default true\n    autoClose?: boolean;\n}\n\ninterface FocusAnyApi {\n\n    /**\n     * set log listener\n     * @param callback\n     */\n    onLog(callback: (label: string, data?: any) => void): void;\n\n    /**\n     * Plugin application initialization complete callback\n     * @param callback\n     */\n    onPluginReady(\n        callback: (data: {\n            actionName: string;\n            actionMatch: ActionMatch | null;\n            actionMatchFiles: FileItem[];\n            requestId: string;\n            reenter: boolean;\n            isView: boolean;\n            type: \"action\" | \"callPage\"\n        }) => void\n    ): void;\n\n    /**\n     * Plugin application exit callback\n     * @param callback\n     */\n    onPluginExit(callback: Function): void;\n\n    /**\n     * Plugin event listener\n     * @param event\n     * @param callback\n     */\n    onPluginEvent(event: PluginEvent, callback: (data: any) => void): void;\n\n    /**\n     * Plugin event unbind\n     * @param event\n     * @param callback\n     */\n    offPluginEvent(event: PluginEvent, callback: (data: any) => void): void;\n\n    /**\n     * Plugin event unbind all\n     * @param event\n     */\n    offPluginEventAll(event: PluginEvent): void;\n\n    /**\n     * plugin more menu click\n     * @param callback\n     */\n    onMoreMenuClick(callback: (data: { name: string }) => void): void;\n\n    /**\n     * register hotkey\n     * @param key\n     * @param callback\n     */\n    registerHotkey(key: string | string[] | HotkeyQuickType | HotkeyType | HotkeyType[], callback: () => void): void;\n\n    /**\n     * unregister all hotkey\n     */\n    unregisterHotkeyAll(): void;\n\n    /**\n     * Check if plugin main window is shown\n     */\n    isMainWindowShown(): boolean;\n\n    /**\n     * Hide plugin main window\n     */\n    hideMainWindow(): void;\n\n    /**\n     * Show plugin main window\n     */\n    showMainWindow(): void;\n\n    /**\n     * Check if fast panel window is shown\n     */\n    isFastPanelWindowShown(): boolean;\n\n    /**\n     * Show fast panel window\n     */\n    showFastPanelWindow(): void;\n\n    /**\n     * Hide fast panel window\n     */\n    hideFastPanelWindow(): void;\n\n    /**\n     * Set plugin height\n     * @param height\n     */\n    setExpendHeight(height: number): void;\n\n    /**\n     * Set input box listener\n     * @param onChange\n     * @param placeholder\n     * @param isFocus\n     * @param isVisible\n     */\n    setSubInput(\n        onChange: (keywords: string) => void,\n        placeholder?: string,\n        isFocus?: boolean,\n        isVisible?: boolean\n    ): void;\n\n    /**\n     * Remove input box listener\n     */\n    removeSubInput(): void;\n\n    /**\n     * Set sub input box value\n     * @param value\n     */\n    setSubInputValue(value: string): void;\n\n    /**\n     * Sub input box lose focus\n     */\n    subInputBlur(): void;\n\n    /**\n     * Get plugin root directory\n     */\n    getPluginRoot(): string;\n\n    /**\n     * Get plugin configuration\n     */\n    getPluginConfig(): {\n        name: string;\n        title: string;\n        version: string;\n        logo: string;\n    } | null;\n\n    /**\n     * Get plugin information\n     */\n    getPluginInfo(): {\n        nodeIntegration: boolean;\n        preloadBase: string;\n        preload: string;\n        main: string;\n        mainView: string;\n        width: number;\n        height: number;\n        autoDetach: boolean;\n        singleton: boolean;\n        zoom: number;\n    };\n\n    /**\n     * Get plugin environment\n     */\n    getPluginEnv(): \"dev\" | \"prod\";\n\n    /**\n     * Get plugin query information\n     * @param requestId\n     */\n    getQuery(requestId: string): SearchQuery;\n\n    /**\n     * Create browser window\n     * @param url\n     * @param options\n     * @param callback\n     */\n    createBrowserWindow(\n        url: string,\n        options: BrowserWindow.InitOptions,\n        callback?: () => void\n    ): BrowserWindow.WindowInstance;\n\n    /**\n     * Close plugin\n     */\n    outPlugin(): void;\n\n    /**\n     * Check if dark theme\n     */\n    isDarkColors(): boolean;\n\n    /**\n     * Show user login dialog\n     */\n    showUserLogin(): void;\n\n    /**\n     * Get user information\n     */\n    getUser(): {\n        isLogin: boolean;\n        avatar: string;\n        nickname: string;\n        vipFlag: string;\n        deviceCode: string;\n        openId: string;\n    };\n\n    /**\n     * Get user server-side temporary token\n     */\n    getUserAccessToken(): Promise<{\n        token: string;\n        expireAt: number;\n    }>;\n\n    /**\n     * List plugin goods\n     * @param query\n     */\n    listGoods(query?: { ids?: string[] }): Promise<\n        {\n            id: string;\n            title: string;\n            cover: string;\n            priceType: \"fixed\" | \"dynamic\";\n            fixedPrice: string;\n            description: string;\n        }[]\n    >;\n\n    /**\n     * Create order and display payment\n     * @param options Order parameters\n     */\n    openGoodsPayment(options: {\n        /**\n         * Plugin goods ID\n         */\n        goodsId: string;\n        /**\n         * Plugin goods price, no need to pass for fixed price goods, dynamic price goods need to pass price, e.g. 0.01\n         */\n        price?: string;\n        /**\n         * Third-party order ID, string, max length 64 characters\n         */\n        outOrderId?: string;\n        /**\n         * Parameter data, length not exceeding 200 characters\n         */\n        outParam?: string;\n    }): Promise<{\n        /**\n         * Whether payment is successful\n         */\n        paySuccess: boolean;\n    }>;\n\n    /**\n     * Query plugin goods orders\n     * @param options\n     */\n    queryGoodsOrders(options: {\n        /**\n         * Plugin goods ID, optional\n         */\n        goodsId?: string;\n        /**\n         * Page number, starting from 1, optional\n         */\n        page?: number;\n        /**\n         * Page size, optional, default is 10\n         */\n        pageSize?: number;\n    }): Promise<{\n        /**\n         * Current page number\n         */\n        page: number;\n        /**\n         * Total number of orders\n         */\n        total: number;\n        /**\n         * Order list\n         */\n        records: {\n            /**\n             * Order ID\n             */\n            id: string;\n            /**\n             * Goods ID\n             */\n            goodsId: string;\n            /**\n             * Status: Paid: Paid, Unpaid: Unpaid\n             */\n            status: \"Paid\" | \"Unpaid\";\n        }[];\n    }>;\n\n    /**\n     * Request official API\n     */\n    apiPost(url: string, body: any, option: {}): Promise<BaseResult>;\n\n    /**\n     * Dynamically set plugin action\n     * @param action\n     */\n    setAction(action: PluginAction | PluginAction[]): void;\n\n    /**\n     * Remove plugin action\n     * @param name\n     */\n    removeAction(name: string): void;\n\n    /**\n     * Get plugin actions\n     * @param names\n     */\n    getActions(names?: string[]): PluginAction[];\n\n    /**\n     * Open plugin action\n     * @param keywordsOrAction\n     * @param query\n     */\n    redirect(keywordsOrAction: string | string[], query?: SearchQuery): void;\n\n    /**\n     * Show toast notification\n     * @param body\n     * @param options\n     */\n    showToast(\n        body: string,\n        options?: {\n            duration?: number;\n            status?: \"info\" | \"success\" | \"error\";\n        }\n    ): void;\n\n    /**\n     * Show notification\n     * @param body\n     * @param clickActionName\n     */\n    showNotification(body: string, clickActionName?: string): void;\n\n    /**\n     * Show message box\n     * @param message\n     * @param options\n     * @return true if \"yes\" is clicked, false if \"no\" is clicked\n     */\n    showMessageBox(\n        message: string,\n        options: {\n            title?: string;\n            yes?: string;\n            no?: string;\n        }\n    ): boolean;\n\n    /**\n     * Show open file dialog\n     * @param options\n     */\n    showOpenDialog(options: {\n        title?: string;\n        defaultPath?: string;\n        buttonLabel?: string;\n        filters?: { name: string; extensions: string[] }[];\n        properties?: Array<\n            | \"openFile\"\n            | \"openDirectory\"\n            | \"multiSelections\"\n            | \"showHiddenFiles\"\n            | \"createDirectory\"\n            | \"promptToCreate\"\n            | \"noResolveAliases\"\n            | \"treatPackageAsDirectory\"\n            | \"dontAddToRecent\"\n        >;\n        message?: string;\n        securityScopedBookmarks?: boolean;\n    }): string[] | undefined;\n\n    /**\n     * Show save file dialog\n     * @param options\n     */\n    showSaveDialog(options: {\n        title?: string;\n        defaultPath?: string;\n        buttonLabel?: string;\n        filters?: { name: string; extensions: string[] }[];\n        message?: string;\n        nameFieldLabel?: string;\n        showsTagField?: string;\n        properties?: Array<\n            | \"showHiddenFiles\"\n            | \"createDirectory\"\n            | \"treatPackageAsDirectory\"\n            | \"showOverwriteConfirmation\"\n            | \"dontAddToRecent\"\n        >;\n        securityScopedBookmarks?: boolean;\n    }): string | undefined;\n\n    /**\n     * Take screenshot\n     * @param callback\n     */\n    screenCapture(callback: (imgBase64: string) => void): void;\n\n    /**\n     * Get device ID\n     */\n    getNativeId(): string;\n\n    /**\n     * Get software version\n     */\n    getAppVersion(): string;\n\n    /**\n     * Get system path\n     * @param name\n     */\n    getPath(\n        name:\n            | \"home\"\n            | \"appData\"\n            | \"userData\"\n            | \"temp\"\n            | \"exe\"\n            | \"desktop\"\n            | \"documents\"\n            | \"downloads\"\n            | \"music\"\n            | \"pictures\"\n            | \"videos\"\n            | \"logs\"\n    ): string;\n\n    /**\n     * Get file icon as Base64\n     * @param path\n     */\n    getFileIcon(path: string): string;\n\n    /**\n     * Copy file to clipboard\n     * @param file\n     */\n    copyFile(file: string | string[]): boolean;\n\n    /**\n     * Copy image to clipboard\n     * @param image\n     */\n    copyImage(image: string): boolean;\n\n    /**\n     * Copy text to clipboard\n     * @param text\n     */\n    copyText(text: string): boolean;\n\n    /**\n     * Get clipboard text\n     */\n    getClipboardText(): string;\n\n    /**\n     * Get clipboard image\n     */\n    getClipboardImage(): string;\n\n    /**\n     * Get clipboard files\n     */\n    getClipboardFiles(): FileItem[];\n\n    /**\n     * List clipboard history\n     */\n    listClipboardItems(option?: { limit?: number }): Promise<{\n        type: \"file\" | \"image\" | \"text\";\n        timestamp: number;\n        files?: FileItem[];\n        image?: string;\n        text?: string;\n    }[]>;\n\n    /**\n     * Delete clipboard item by timestamp\n     * @param timestamp\n     */\n    deleteClipboardItem(timestamp: number): Promise<void>;\n\n    /**\n     * Clear clipboard history\n     */\n    clearClipboardItems(): Promise<void>;\n\n    /**\n     * Open file with default application\n     * @param fullPath\n     */\n    shellOpenPath(fullPath: string): void;\n\n    /**\n     * Show file in file manager\n     * @param fullPath\n     */\n    shellShowItemInFolder(fullPath: string): void;\n\n    /**\n     * Open URL with external browser\n     * @param url\n     */\n    shellOpenExternal(url: string): void;\n\n    /**\n     * Play system beep sound\n     */\n    shellBeep(): void;\n\n    /**\n     * simulate user input\n     */\n    simulate: {\n        /**\n         * simulate keyboard tap\n         * @param key\n         * @param modifiers\n         */\n        keyboardTap(key: string, modifiers: (\"ctrl\" | \"shift\" | \"command\" | \"option\" | \"alt\")[]): Promise<void>;\n        /**\n         * simulate type string\n         * @param text\n         */\n        typeString(text: string): Promise<void>;\n        /**\n         * simulate mouse toggle\n         * @param type\n         * @param button\n         */\n        mouseToggle(type: \"down\" | \"up\", button: \"left\" | \"right\" | \"middle\"): Promise<void>;\n        /**\n         * simulate mouse move\n         * @param x\n         * @param y\n         */\n        mouseMove(x: number, y: number): Promise<void>;\n        /**\n         * simulate mouse click\n         * @param button\n         * @param double\n         */\n        mouseClick(button: \"left\" | \"right\" | \"middle\", double?: boolean): Promise<void>;\n    };\n\n    /**\n     * Get cursor screen position\n     */\n    getCursorScreenPoint(): { x: number; y: number };\n\n    /**\n     * Get display nearest to point\n     * @param point\n     */\n    getDisplayNearestPoint(point: { x: number; y: number }): any;\n\n    /**\n     * Check if running on macOS\n     */\n    isMacOs(): boolean;\n\n    /**\n     * Check if running on Windows\n     */\n    isWindows(): boolean;\n\n    /**\n     * Check if running on Linux\n     */\n    isLinux(): boolean;\n\n    /**\n     * Get platform architecture\n     */\n    getPlatformArch(): \"x86\" | \"arm64\" | null;\n\n    /**\n     * Send backend event\n     * @param event\n     * @param data\n     * @param option\n     */\n    sendBackendEvent(\n        event: string,\n        data?: any,\n        option?: {\n            timeout: number;\n        }\n    ): Promise<any>;\n\n    /**\n     * Register backend caller in web\n     * @param type\n     * @param callback\n     * @param option\n     */\n    registerCallPage<DataInput extends any, DataOutput extends any>(\n        type: string,\n        callback: (\n            resolve: (data: DataOutput) => void,\n            reject: (error: string) => void,\n            data: DataInput\n        ) => void,\n        option?: {\n            timeout?: number;\n        }\n    ): void;\n\n    /**\n     * call backend from backend.cjs script\n     * @param type\n     * @param data\n     * @param option\n     */\n    callPage<DataInput extends any, DataOutput extends any>(\n        type: string,\n        data?: DataInput,\n        option?: CallPageOption\n    ): Promise<DataOutput>;\n\n    /**\n     * set remote web runtime\n     * @param info\n     */\n    setRemoteWebRuntime(info: {\n        userAgent: string;\n        urlMap: Record<string, string>;\n        types: string[];\n        domains: string[];\n        blocks: string[];\n    }): Promise<undefined>;\n\n    /**\n     * list large language model\n     */\n    llmListModels(): Promise<\n        {\n            providerId: string;\n            providerLogo: string;\n            providerTitle: string;\n            modelId: string;\n            modelName: string;\n        }[]\n    >;\n\n    /**\n     * call large language model chat\n     * @param callInfo\n     */\n    llmChat(callInfo: { providerId: string; modelId: string; message: string }): Promise<\n        BaseResult<{\n            message: string;\n        }>\n    >;\n\n    /**\n     * write info log\n     * @param label\n     * @param data\n     */\n    logInfo(label: string, data?: any): void;\n\n    /**\n     * write error log\n     * @param label\n     * @param data\n     */\n    logError(label: string, data?: any): void;\n\n    /**\n     * get log file path\n     */\n    logPath(): Promise<string>;\n\n    /**\n     * show log file\n     */\n    logShow(): void;\n\n    /**\n     * add launch\n     * @param keyword\n     * @param name\n     * @param hotkey\n     */\n    addLaunch(\n        keyword: string,\n        name: string,\n        hotkey: HotkeyType\n    ): Promise<void>;\n\n    /**\n     * remove launch\n     * @param keyword\n     */\n    removeLaunch(keyword: string): void;\n\n    /**\n     * activate latest window\n     */\n    activateLatestWindow(): Promise<void>;\n\n    /**\n     * File operations\n     */\n    file: {\n        /**\n         * Check if file or directory exists\n         * @param path\n         */\n        exists(path: string): Promise<boolean>;\n        /**\n         * Read file content\n         * @param path File path\n         * @param format File content format, default is 'string'\n         */\n        read(\n            path: string,\n            format?: 'string' | 'buffer' | 'base64'\n        ): Promise<string | Uint8Array>;\n        /**\n         * Write file content\n         * @param path File path\n         * @param data File content\n         * @param option Write options\n         */\n        write(\n            path: string,\n            data: string | Uint8Array,\n            option?: {\n                isBase64?: boolean;\n            }\n        ): Promise<void>;\n        /**\n         * Delete file or directory\n         * @param path File or directory path\n         */\n        remove(path: string): Promise<void>;\n        /**\n         * Get file extension\n         */\n        ext(path: string): Promise<string>;\n        /**\n         * save file to temp path\n         */\n        writeTemp(\n            ext: string,\n            data: string | Uint8Array,\n            option?: {\n                isBase64?: boolean;\n            }\n        ): Promise<string>;\n    };\n\n    /**\n     * Database operations\n     */\n    db: {\n        /**\n         * Add document\n         * @param doc\n         */\n        put(doc: DbDoc): DbReturn;\n        /**\n         * Get document\n         * @param id\n         */\n        get<T extends {} = Record<string, any>>(id: string): DbDoc<T> | null;\n        /**\n         * Delete document\n         * @param doc\n         */\n        remove(doc: string | DbDoc): DbReturn;\n        /**\n         * Bulk add documents\n         * @param docs\n         */\n        bulkDocs(docs: DbDoc[]): DbReturn[];\n        /**\n         * Bulk get documents\n         * @param key\n         */\n        allDocs<T extends {} = Record<string, any>>(key?: string): DbDoc<T>[];\n        /**\n         * Save attachment\n         * @param docId\n         * @param attachment\n         * @param type\n         */\n        postAttachment(docId: string, attachment: Uint8Array, type: string): DbReturn;\n        /**\n         * Get attachment\n         * @param docId\n         */\n        getAttachment(docId: string): Uint8Array | null;\n        /**\n         * Get attachment type\n         * @param docId\n         */\n        getAttachmentType(docId: string): string | null;\n    };\n\n    /**\n     * Local storage\n     */\n    dbStorage: {\n        /**\n         * Set storage\n         * @param key\n         * @param value\n         */\n        setItem(key: string, value: any): void;\n        /**\n         * Get storage\n         * @param key\n         */\n        getItem<T = any>(key: string): T;\n        /**\n         * Remove storage\n         * @param key\n         */\n        removeItem(key: string): void;\n    };\n\n    /**\n     * Fast access documents\n     */\n    fad: {\n        /**\n         * Read fast access document content\n         * @param type\n         * @param path\n         */\n        read(type: string, path: string): Promise<any>;\n        /**\n         * Write fast access document content\n         * @param type\n         * @param path\n         * @param data\n         */\n        write(type: string, path: string, data: any): Promise<void>;\n    };\n\n    /**\n     * Quick panel view\n     */\n    view: {\n        /**\n         * Set height of current plugin render area in quick panel\n         * @param height\n         */\n        setHeight(height: number): void;\n        /**\n         * Get height of current plugin render area in quick panel\n         */\n        getHeight(): Promise<number>;\n    };\n\n    /**\n     * Detached window\n     */\n    detach: {\n        /**\n         * Set detached window title\n         * @param title\n         */\n        setTitle(title: string): void;\n        /**\n         * set the detach window actions\n         * @param operates\n         */\n        setOperates(\n            operates: {\n                name: string;\n                title: string;\n                click: () => void;\n            }[]\n        ): void;\n        /**\n         * Set detached window position\n         * @param position\n         */\n        setPosition(position: \"center\" | \"right-bottom\" | \"left-top\" | \"right-top\" | \"left-bottom\"): void;\n        /**\n         * Set detached window always on top\n         * @param alwaysOnTop\n         */\n        setAlwaysOnTop(alwaysOnTop: boolean): void;\n        /**\n         * Set detached window size\n         */\n        setSize(width: number, height: number): void;\n    };\n\n    /**\n     * Utilities\n     */\n    util: {\n        /**\n         * Generate random string\n         * @param length\n         */\n        randomString(length: number): string;\n        /**\n         * Convert Buffer to Base64\n         * @param buffer\n         */\n        bufferToBase64(buffer: Uint8Array): string;\n        /**\n         * Convert Base64 to Buffer\n         */\n        base64ToBuffer(base64: string): Uint8Array;\n        /**\n         * Get current timestamp string\n         */\n        datetimeString(): string;\n        /**\n         * Convert data to Base64\n         * @param data\n         */\n        base64Encode(data: any): string;\n        /**\n         * Convert Base64 to data\n         * @param data\n         */\n        base64Decode(data: string): any;\n        /**\n         * MD5 hash\n         * @param data\n         */\n        md5(data: string): string;\n        /**\n         * Save file\n         * @param filename\n         * @param data\n         * @param option\n         */\n        save(\n            filename: string,\n            data: string | Uint8Array,\n            option?: {\n                isBase64?: boolean;\n            }\n        ): boolean;\n    };\n}\n\ndeclare var focusany: FocusAnyApi;\n"
  },
  {
    "path": "sdk/index.d.ts",
    "content": "/// <reference path=\"focusany.d.ts\" />\n\n// 默认导出 focusany API\ndeclare const focusany: FocusAnyApi;\nexport = focusany;\n"
  },
  {
    "path": "sdk/index.ts",
    "content": "/// <reference path=\"focusany.d.ts\" />\n\nexport = focusany\n"
  },
  {
    "path": "sdk/package.json",
    "content": "{\n    \"name\": \"focusany-sdk\",\n    \"version\": \"1.3.8\",\n    \"description\": \"TypeScript definitions for FocusAny\",\n    \"main\": \"./index.js\",\n    \"types\": \"./index.d.ts\",\n    \"bin\": {\n    \"focusany\": \"./bin/command.js\"\n  },\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/modstart-lib/focusany\"\n    },\n    \"keywords\": [\n        \"FocusAny\"\n    ],\n    \"author\": \"ModStart\",\n    \"license\": \"Apache-2.0\",\n    \"bugs\": {\n        \"url\": \"https://github.com/modstart-lib/focusany/issues\"\n    },\n    \"homepage\": \"https://github.com/modstart-lib/focusany#readme\",\n    \"scripts\": {\n        \"build\": \"npx tsc --project tsconfig.json\",\n        \"test\": \"node tests/test-release-prepare.js\"\n    },\n    \"exports\": {\n        \".\": {\n            \"types\": \"./index.d.ts\",\n            \"default\": \"./index.js\"\n        },\n        \"./shim\": {\n            \"types\": \"./focusany-shim.d.ts\",\n            \"default\": \"./focusany-shim.js\"\n        }\n    },\n    \"devDependencies\": {\n        \"@babel/cli\": \"^7.28.0\",\n        \"@babel/core\": \"^7.28.0\",\n        \"@babel/preset-env\": \"^7.28.0\",\n        \"@babel/preset-typescript\": \"^7.27.1\",\n        \"@types/node\": \"^20.0.0\",\n        \"terser\": \"^5.43.1\",\n        \"typescript\": \"^5.0.0\"\n    }\n}\n"
  },
  {
    "path": "sdk/shim.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>FocusAny Shim Test</title>\n    <style>\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n            padding: 20px;\n            background: #f5f5f5;\n            margin: 0;\n        }\n\n        .container {\n            max-width: 800px;\n            margin: 0 auto;\n            background: white;\n            padding: 30px;\n            border-radius: 10px;\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\n        }\n\n        h1 {\n            color: #333;\n            margin-bottom: 10px;\n        }\n\n        .subtitle {\n            color: #666;\n            margin-bottom: 30px;\n            font-size: 16px;\n        }\n\n        .button-group {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 10px;\n            margin-bottom: 30px;\n        }\n\n        button {\n            background: linear-gradient(135deg, #007AFF, #0056CC);\n            color: white;\n            border: none;\n            padding: 12px 24px;\n            border-radius: 8px;\n            cursor: pointer;\n            font-size: 14px;\n            font-weight: 500;\n            transition: all 0.3s ease;\n            min-width: 120px;\n        }\n\n        button:hover {\n            background: linear-gradient(135deg, #0056CC, #003D99);\n            transform: translateY(-2px);\n            box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);\n        }\n\n        button:active {\n            transform: translateY(0);\n        }\n\n        .success {\n            background: linear-gradient(135deg, #52c41a, #389e0d);\n        }\n\n        .error {\n            background: linear-gradient(135deg, #ff4d4f, #cf1322);\n        }\n\n        .info {\n            background: linear-gradient(135deg, #1890ff, #096dd9);\n        }\n\n        #log {\n            background: #f8f9fa;\n            border: 1px solid #e9ecef;\n            border-radius: 8px;\n            padding: 20px;\n            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n            font-size: 13px;\n            line-height: 1.5;\n            max-height: 400px;\n            overflow-y: auto;\n            white-space: pre-wrap;\n            word-wrap: break-word;\n        }\n\n        .status {\n            padding: 15px;\n            border-radius: 8px;\n            margin-bottom: 20px;\n            border-left: 4px solid #2196f3;\n            background: #e3f2fd;\n        }\n\n        .log-entry {\n            margin-bottom: 5px;\n            padding: 2px 0;\n        }\n\n        .log-time {\n            color: #666;\n            font-weight: bold;\n        }\n\n        .log-success {\n            color: #52c41a;\n        }\n\n        .log-error {\n            color: #ff4d4f;\n        }\n\n        .log-info {\n            color: #1890ff;\n        }\n    </style>\n</head>\n\n<body>\n    <div class=\"container\">\n        <h1>🚀 FocusAny Shim Version Test</h1>\n        <div class=\"status\">\n            <strong>Status:</strong> <span id=\"status\">Initializing...</span>\n        </div>\n\n        <div class=\"button-group\">\n            <button onclick=\"testBasicNotification()\">📢 Basic Notification</button>\n            <button onclick=\"testSuccessToast()\" class=\"success\">✅ Success Toast</button>\n            <button onclick=\"testErrorToast()\" class=\"error\">❌ Error Toast</button>\n            <button onclick=\"testInfoToast()\" class=\"info\">ℹ️ Info Toast</button>\n            <button onclick=\"testLongDuration()\">⏰ Long Duration</button>\n            <button onclick=\"testZeroDuration()\">🔄 Permanent Notification</button>\n            <button onclick=\"testCopyText()\">📋 Copy Text</button>\n            <button onclick=\"testPlatformDetection()\">💻 Platform Detection</button>\n            <button onclick=\"testDbOperations()\">💾 Database Operations</button>\n            <button onclick=\"clearLog()\">🗑️ Clear Log</button>\n        </div>\n\n        <h3>📋 Execution Log:</h3>\n        <div id=\"log\">Waiting for test...</div>\n    </div>\n\n    <!-- Load Babel-transformed FocusAny Shim -->\n    <script src=\"focusany-shim-browser.min.js\"></script>\n\n    <script>\n        // 日志记录函数\n        function addLog(message, type = 'info') {\n            const log = document.getElementById('log');\n            const time = new Date().toLocaleTimeString();\n            const logEntry = document.createElement('div');\n            logEntry.className = `log-entry log-${type}`;\n            logEntry.innerHTML = `<span class=\"log-time\">[${time}]</span> ${message}`;\n            log.appendChild(logEntry);\n            log.scrollTop = log.scrollHeight;\n        }\n\n        function clearLog() {\n            document.getElementById('log').innerHTML = '';\n            addLog('Log cleared');\n        }\n\n        function updateStatus(message, type = 'info') {\n            const status = document.getElementById('status');\n            status.textContent = message;\n            status.className = `log-${type}`;\n        }\n\n        // Test functions\n        function testBasicNotification() {\n            addLog('=== Testing Basic Notification ===');\n            try {\n                window.focusany.showNotification(\"This is a basic notification test\");\n                addLog('✅ Basic notification call successful', 'success');\n            } catch (e) {\n                addLog('❌ Basic notification call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testSuccessToast() {\n            addLog('=== Testing Success Toast ===');\n            try {\n                window.focusany.showToast(\"Operation executed successfully!\", { status: \"success\", duration: 3000 });\n                addLog('✅ Success toast call successful', 'success');\n            } catch (e) {\n                addLog('❌ Success toast call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testErrorToast() {\n            addLog('=== Testing Error Toast ===');\n            try {\n                window.focusany.showToast(\"An error occurred!\", { status: \"error\", duration: 4000 });\n                addLog('✅ Error toast call successful', 'success');\n            } catch (e) {\n                addLog('❌ Error toast call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testInfoToast() {\n            addLog('=== Testing Info Toast ===');\n            try {\n                window.focusany.showToast(\"This is an information message\", { status: \"info\", duration: 2000 });\n                addLog('✅ Info toast call successful', 'success');\n            } catch (e) {\n                addLog('❌ Info toast call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testLongDuration() {\n            addLog('=== Testing Long Duration Notification ===');\n            try {\n                window.focusany.showToast(\"This notification will display for 10 seconds\", { duration: 10000 });\n                addLog('✅ Long duration notification call successful', 'success');\n            } catch (e) {\n                addLog('❌ Long duration notification call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testZeroDuration() {\n            addLog('=== Testing Permanent Notification ===');\n            try {\n                window.focusany.showToast(\"This notification won't disappear automatically, click X to close\", { duration: 0 });\n                addLog('✅ Permanent notification call successful', 'success');\n            } catch (e) {\n                addLog('❌ Permanent notification call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testCopyText() {\n            addLog('=== Testing Copy Text ===');\n            try {\n                const result = window.focusany.copyText(\"Hello from FocusAny Shim!\");\n                if (result) {\n                    addLog('✅ Text copied successfully', 'success');\n                    window.focusany.showToast(\"Text has been copied to clipboard\", { status: \"success\" });\n                } else {\n                    addLog('❌ Text copy failed', 'error');\n                }\n            } catch (e) {\n                addLog('❌ Copy text call failed: ' + e.message, 'error');\n            }\n        }\n\n        function testPlatformDetection() {\n            addLog('=== Testing Platform Detection ===');\n            try {\n                const isMac = window.focusany.isMacOs();\n                const isWindows = window.focusany.isWindows();\n                const isLinux = window.focusany.isLinux();\n\n                addLog(`macOS: ${isMac}, Windows: ${isWindows}, Linux: ${isLinux}`);\n                addLog('✅ Platform detection completed', 'success');\n\n                let platform = 'Unknown';\n                if (isMac) platform = 'macOS';\n                else if (isWindows) platform = 'Windows';\n                else if (isLinux) platform = 'Linux';\n\n                window.focusany.showToast(`Detected platform: ${platform}`, { status: \"info\" });\n            } catch (e) {\n                addLog('❌ Platform detection failed: ' + e.message, 'error');\n            }\n        }\n\n        function testDbOperations() {\n            addLog('=== Testing Database Operations ===');\n            try {\n                // Test write\n                const doc = {\n                    _id: 'test-' + Date.now(),\n                    name: 'Test Document',\n                    content: 'This is a test document',\n                    timestamp: new Date().toISOString()\n                };\n\n                const putResult = window.focusany.db.put(doc);\n                addLog(`Document written: ${JSON.stringify(putResult)}`);\n\n                // Test read\n                const getResult = window.focusany.db.get(doc._id);\n                addLog(`Document read: ${JSON.stringify(getResult)}`);\n\n                // Test query all\n                const allDocs = window.focusany.db.allDocs();\n                addLog(`Total documents: ${allDocs.length}`);\n\n                addLog('✅ Database operations test completed', 'success');\n                window.focusany.showToast(\"Database operations test completed\", { status: \"success\" });\n            } catch (e) {\n                addLog('❌ Database operations failed: ' + e.message, 'error');\n            }\n        }\n\n        // Check after page load is complete\n        document.addEventListener('DOMContentLoaded', function () {\n            addLog('Page loading complete');\n\n            setTimeout(() => {\n                if (window.focusany) {\n                    addLog('✅ FocusAny Shim loaded successfully', 'success');\n                    updateStatus('✅ FocusAny Shim ready', 'success');\n\n                    // Test available methods\n                    const methods = ['showNotification', 'showToast', 'copyText', 'isMacOs', 'isWindows', 'isLinux'];\n                    const availableMethods = methods.filter(method => {\n                        try {\n                            return typeof window.focusany[method] === 'function';\n                        } catch (e) {\n                            return false;\n                        }\n                    });\n\n                    addLog(`Available methods: ${availableMethods.join(', ')}`);\n\n                    // Automatically display welcome message\n                    window.focusany.showToast(\"FocusAny Shim Babel version loaded successfully!\", {\n                        status: \"success\",\n                        duration: 3000\n                    });\n                } else {\n                    addLog('❌ FocusAny Shim failed to load', 'error');\n                    updateStatus('❌ FocusAny Shim failed to load', 'error');\n                }\n            }, 100);\n        });\n\n        // Monitor console output\n        const originalLog = console.log;\n        const originalError = console.error;\n\n        console.log = function (...args) {\n            addLog('Console.log: ' + args.join(' '));\n            originalLog.apply(console, args);\n        };\n\n        console.error = function (...args) {\n            addLog('Console.error: ' + args.join(' '), 'error');\n            originalError.apply(console, args);\n        };\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "sdk/tests/config.json",
    "content": "{\n  \"development\": {\n    \"env\": \"dev\",\n    \"debug\": true,\n    \"apiEndpoint\": \"https://api-dev.example.com\"\n  },\n  \"production\": {\n    \"env\": \"prod\",\n    \"debug\": false,\n    \"apiEndpoint\": \"https://api.example.com\"\n  },\n  \"test\": {\n    \"enabled\": true,\n    \"mockResponses\": true,\n    \"timeout\": 5000\n  }\n}"
  },
  {
    "path": "sdk/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2020\",\n        \"module\": \"CommonJS\",\n        \"lib\": [\n            \"ES2020\",\n            \"DOM\"\n        ],\n        \"outDir\": \"./\",\n        \"rootDir\": \"./\",\n        \"strict\": true,\n        \"esModuleInterop\": true,\n        \"skipLibCheck\": true,\n        \"forceConsistentCasingInFileNames\": true,\n        \"declaration\": false,\n        \"moduleResolution\": \"node\"\n    },\n    \"include\": [\n        \"*.ts\",\n        \"*/**.ts\"\n    ],\n    \"exclude\": [\n        \"node_modules\",\n        \"*.js\",\n        \"*/**.js\",\n        \"*.d.ts\"\n    ]\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n    <a-config-provider :locale=\"locale\" :global=\"true\">\n        <div\n            ref=\"main\"\n            id=\"main\"\n            :class=\"{ 'no-active-plugin': !manager.activePlugin }\"\n        >\n            <MainSearch ref=\"mainSearch\" @onClose=\"onClose\" />\n            <MainResult ref=\"mainResult\" />\n        </div>\n    </a-config-provider>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\n\nimport { useLocale } from \"./app/locale\";\nimport { doCheckForUpdate } from \"./components/common/util\";\nimport { useMainOperate } from \"./pages/Main/Lib/mainOperate\";\nimport { ignoreNextResultResize } from \"./pages/Main/Lib/resultResize\";\nimport MainResult from \"./pages/Main/MainResult.vue\";\nimport MainSearch from \"./pages/Main/MainSearch.vue\";\nimport { useManagerStore } from \"./store/modules/manager\";\nimport { PluginRecord } from \"./types/Manager\";\n\nconst manager = useManagerStore();\n\nconst main = ref<HTMLElement | null>(null);\nconst mainSearch = ref<InstanceType<typeof MainSearch> | null>(null);\nconst mainResult = ref<InstanceType<typeof MainResult> | null>(null);\n\nconst { locale } = useLocale();\n\nconst { onKeyDown } = useMainOperate();\n\nconst onClose = () => {\n    mainResult.value?.onClose();\n};\n\nwindow.__page.onShow(() => {\n    // console.log('main.onShow')\n    manager.showFirstRun = true;\n    mainSearch.value?.onShow();\n});\nwindow.__page.onPluginInit(\n    (data: {\n        plugin: PluginRecord;\n        param: {\n            alwaysOnTop: boolean;\n        };\n    }) => {\n        // console.log('main.onPluginInit', data)\n        manager.setActivePluginLoading(true);\n        manager.setActivePlugin(data.plugin);\n        manager.setSubInput({\n            placeholder: \"\",\n            isFocus: false,\n            isVisible: false,\n        });\n        manager.setSubInputValue(\"\");\n        mainSearch.value?.focus(true);\n    },\n);\nwindow.__page.onPluginInitReady(() => {\n    // console.log('main.onPluginInitReady')\n    manager.setActivePluginLoading(false);\n});\nwindow.__page.onPluginAlreadyOpened(() => {\n    // console.log('main.onPluginAlreadyOpened')\n    manager.search(\"\");\n    manager.hideMainWindow();\n});\nwindow.__page.onPluginExit((data: { openForNext: boolean }) => {\n    // console.log('main.onPluginExit', data);\n    if (data.openForNext) {\n        ignoreNextResultResize();\n    }\n    manager.setActivePlugin(null);\n    manager.search(\"\");\n    mainResult.value?.onPluginExit();\n    setTimeout(() => {\n        if (manager.activePlugin) {\n            return;\n        }\n        mainSearch.value?.focus(true);\n    }, 100);\n});\nwindow.__page.onPluginDetached(() => {\n    // console.log('main.onPluginDetached')\n    manager.setActivePlugin(null);\n    manager.search(\"\");\n    mainResult.value?.onPluginDetached();\n});\nwindow.__page.onDetachWindowClosed(async () => {\n    if (!manager.activePlugin) {\n        await manager.detachWindowActionsRefresh();\n    }\n});\nwindow.__page.onPluginState(() => {\n    return {\n        value: manager.searchValue,\n        placeholder: manager.searchSubPlaceholder,\n    };\n});\nwindow.__page.onPluginCodeInit(\n    (data: {\n        plugin: PluginRecord;\n        type: \"list\" | never;\n        placeholder: string;\n    }) => {\n        // console.log('main.onPluginCodeInit', data);\n        manager.setActivePlugin(data.plugin, \"code\");\n        manager.setActivePluginLoading(false);\n        manager.setSubInput({\n            placeholder: data.placeholder,\n            isFocus: true,\n            isVisible: true,\n        });\n        manager.actionCodeType = data.type;\n        setTimeout(() => {\n            mainSearch.value?.focus(false);\n        }, 1000);\n    },\n);\nwindow.__page.onPluginCodeSetting(\n    (data: { loading?: boolean; error?: string; placeholder?: string }) => {\n        // console.log('main.onPluginCodeData', data);\n        if (\"loading\" in data) {\n            manager.actionCodeLoading = data.loading || false;\n        }\n        if (\"error\" in data) {\n            manager.actionCodeError = data.error || \"\";\n        }\n        if (\"placeholder\" in data) {\n            manager.setSubInput({\n                placeholder: data.placeholder as string,\n                isFocus: true,\n                isVisible: true,\n            });\n        }\n    },\n);\nwindow.__page.onPluginCodeData(\n    (data: { items: { id: string; [key: string]: unknown }[] }) => {\n        // console.log('main.onPluginCodeData', data);\n        manager.actionCodeError = null;\n        manager.actionCodeLoading = false;\n        manager.actionCodeItems = data.items.map((o, oIndex) => {\n            return {\n                shortcutIndex: oIndex <= 8 ? oIndex + 1 : -1,\n                ...o,\n            };\n        });\n        manager.actionCodeItemActiveId =\n            data.items.length > 0 ? data.items[0].id : null;\n    },\n);\nwindow.__page.onPluginCodeExit(() => {\n    // console.log('main.onPluginCodeExit');\n    manager.setActivePlugin(null);\n    manager.search(\"\");\n    mainResult.value?.onPluginExit();\n    manager.actionCodeItems = [];\n    manager.actionCodeItemActiveId = null;\n    manager.actionCodeType = null;\n    setTimeout(() => {\n        mainSearch.value?.focus(true);\n    }, 1000);\n});\nwindow.__page.onSetSubInput(\n    (param: { placeholder: string; isFocus: boolean; isVisible: boolean }) => {\n        // console.log('main.onSetSubInput', param)\n        manager.setSubInput(param);\n        if (param.isFocus) {\n            setTimeout(() => {\n                mainSearch.value?.focus(false);\n            }, 1000);\n        }\n    },\n);\nwindow.__page.onRemoveSubInput(() => {\n    // console.log('main.onRemoveSubInput')\n    manager.removeSubInput();\n});\nwindow.__page.onSetSubInputValue((value: string) => {\n    // console.log('main.onSetSubInputValue', value)\n    manager.setSubInputValue(value);\n});\n\nwindow.addEventListener(\"keydown\", (e) => {\n    const { resultKey } = onKeyDown(e);\n    // console.log('main.onKeyDown', e, resultKey);\n    if (resultKey) {\n        mainResult.value?.onInputKey(resultKey);\n    } else if (manager.activePlugin && manager.activePluginType === \"code\") {\n        if ([\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"].includes(e.key)) {\n            mainResult.value?.onInputHotKey(e);\n        }\n    }\n});\n\nonMounted(() => {\n    setTimeout(async () => {\n        const checkAtLaunch = await window.$mapi.config.get(\n            \"updaterCheckAtLaunch\",\n            \"yes\",\n        );\n        if (\"yes\" !== checkAtLaunch) {\n            return;\n        }\n        doCheckForUpdate().then();\n    }, 6000);\n});\n</script>\n\n<style lang=\"less\">\n@mainBorderRadius: 15px;\n#main {\n    height: 100vh;\n    overflow: hidden;\n    border-radius: @mainBorderRadius;\n    background: #ffffff;\n\n    &.no-active-plugin {\n        &::before {\n            content: \"\";\n            position: absolute;\n            inset: 0;\n            padding: 4px;\n            border-radius: @mainBorderRadius;\n            background-image: linear-gradient(\n                130deg,\n                #a8c8f4,\n                #61c4f5,\n                #ba59ff\n            );\n            background-size: 300% 300%;\n            animation: border-flow 2s linear infinite;\n            -webkit-mask:\n                linear-gradient(#fff 0 0) content-box,\n                linear-gradient(#fff 0 0);\n            -webkit-mask-composite: xor;\n            pointer-events: none;\n        }\n\n        @keyframes border-flow {\n            0% {\n                background-position: 0% 50%;\n            }\n            50% {\n                background-position: 100% 50%;\n            }\n            100% {\n                background-position: 0% 50%;\n            }\n        }\n    }\n}\n\n[data-theme=\"dark\"] {\n    #main {\n        background: #1e1e1e;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/api/types/base.ts",
    "content": "interface ApiResult<T> {\n    code: number;\n    msg: string;\n    data: T;\n}\n"
  },
  {
    "path": "src/api/user.ts",
    "content": "import { request } from \"../lib/api\";\n\nexport function userInfoApi(): Promise<\n    ApiResult<{\n        apiToken: string;\n        user: object;\n        data: any;\n        basic: object;\n    }>\n> {\n    return request({\n        url: \"app_manager/user_info\",\n        method: \"post\",\n    });\n}\n"
  },
  {
    "path": "src/app/dragWindow.ts",
    "content": "export const useDragWindow = ({\n    name,\n    ignore,\n}: {\n    name: string | null;\n    ignore?: (e: MouseEvent) => boolean;\n}) => {\n    name = name || null;\n    let animationId: number;\n    let mouseX: number;\n    let mouseY: number;\n    let clientWidth = 0;\n    let clientHeight = 0;\n    let draggable = true;\n\n    const onDragWindowMouseDown = (e) => {\n        // 右击不移动\n        if (e.button === 2) {\n            return;\n        }\n        if (ignore && ignore(e)) {\n            return;\n        }\n        draggable = true;\n        mouseX = e.clientX;\n        mouseY = e.clientY;\n        if (Math.abs(document.body.clientWidth - clientWidth) > 5) {\n            clientWidth = document.body.clientWidth;\n        }\n        if (Math.abs(document.body.clientHeight - clientHeight) > 5) {\n            clientHeight = document.body.clientHeight;\n        }\n        document.addEventListener(\"mouseup\", onMouseUp);\n        animationId = requestAnimationFrame(moveWindow);\n    };\n\n    const onMouseUp = () => {\n        draggable = false;\n        document.removeEventListener(\"mouseup\", onMouseUp);\n        cancelAnimationFrame(animationId);\n    };\n\n    const moveWindow = () => {\n        window.$mapi.app\n            .windowMove(name, {\n                mouseX,\n                mouseY,\n                width: clientWidth,\n                height: clientHeight,\n            })\n            .then(() => {\n                if (draggable) {\n                    animationId = requestAnimationFrame(moveWindow);\n                }\n            });\n    };\n\n    return {\n        onDragWindowMouseDown,\n    };\n};\n"
  },
  {
    "path": "src/app/locale.ts",
    "content": "import zhCN from \"@arco-design/web-vue/es/locale/lang/zh-cn\";\nimport enUS from \"@arco-design/web-vue/es/locale/lang/en-us\";\nimport { onLocaleChange } from \"../lang\";\nimport { ref } from \"vue\";\n\nexport const useLocale = () => {\n    const locales = {\n        \"zh-CN\": zhCN,\n        \"en-US\": enUS,\n    };\n    const locale = ref(zhCN);\n    onLocaleChange((newLocale) => {\n        locale.value = locales[newLocale];\n    });\n    return {\n        locale: locale,\n    };\n};\n"
  },
  {
    "path": "src/components/AppQuitConfirm.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useSettingStore } from \"../store/modules/setting\";\n\nconst visible = ref(false);\nconst remember = ref(false);\nconst setting = useSettingStore();\nconst exitMode = setting.configGet(\"exitMode\", \"\");\n\nconst show = async () => {\n    if (exitMode.value) {\n        if (exitMode.value === \"exit\") {\n            await doExit();\n        } else if (exitMode.value === \"hide\") {\n            await doHide();\n        }\n        return;\n    }\n    visible.value = true;\n};\n\nconst doCancel = () => {\n    visible.value = false;\n};\n\nconst doHide = async () => {\n    if (remember.value) {\n        await setting.setConfig(\"exitMode\", \"hide\");\n    }\n    visible.value = false;\n    setTimeout(async () => {\n        await window.$mapi.app.windowHide();\n    }, 100);\n};\n\nconst doExit = async () => {\n    if (remember.value) {\n        await setting.setConfig(\"exitMode\", \"exit\");\n    }\n    visible.value = false;\n    await window.$mapi.app.quit();\n};\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"22rem\"\n        modal-class=\"pb-app-quit-confirm\"\n        :closable=\"true\"\n        :title=\"$t('common.tip')\"\n        title-align=\"start\"\n    >\n        <template #footer>\n            <a-button @click=\"doCancel\">{{ $t(\"common.cancel\") }}</a-button>\n            <a-button @click=\"doExit\">{{ $t(\"common.exit\") }}</a-button>\n            <a-button type=\"primary\" @click=\"doHide\">{{\n                $t(\"common.hideWindow\")\n            }}</a-button>\n        </template>\n        <div>\n            <div class=\"text-center\">{{ $t(\"common.exitConfirm\") }}</div>\n            <div class=\"text-center mt-4\">\n                <a-checkbox v-model=\"remember\">\n                    <span class=\"text-sm text-gray-500\">{{\n                        $t(\"common.rememberChoice\")\n                    }}</span>\n                </a-checkbox>\n            </div>\n        </div>\n    </a-modal>\n</template>\n\n<style lang=\"less\">\n.pb-app-quit-confirm {\n    .arco-modal-header {\n        border-bottom: none;\n    }\n\n    .arco-modal-footer {\n        border-top: none;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/components/PageNav.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport IconAccount from \"~icons/mdi/account\";\nimport IconServer from \"~icons/mdi/server\";\nimport { t } from \"../lang\";\nimport { useSettingStore } from \"../store/modules/setting\";\nimport { useUserStore } from \"../store/modules/user\";\n\nconst route = useRouter();\nconst user = useUserStore();\nconst setting = useSettingStore();\n\nconst activeTab = computed(() => {\n    switch (route.currentRoute.value.path) {\n        case \"/home\":\n            return \"home\";\n        case \"/server\":\n            return \"server\";\n        // case '/sound':\n        //     return 'sound'\n        case \"/setting\":\n            return \"setting\";\n        case \"/video\":\n            return \"video\";\n    }\n});\n\nconst userTip = computed(() => {\n    return user.user.id ? user.user.name : t(\"common.notLoggedIn\");\n});\n\nconst doUser = async () => {\n    if (!setting.basic.userEnable) {\n        return;\n    }\n    await window.$mapi.user.open();\n};\n</script>\n\n<template>\n    <div class=\"flex flex-col h-full border-r border-default\">\n        <div\n            class=\"py-4 px-3\"\n            :class=\"setting.basic.userEnable ? 'cursor-pointer' : ''\"\n            @click=\"doUser\"\n        >\n            <a-tooltip\n                v-if=\"setting.basic.userEnable\"\n                :content=\"userTip as string\"\n                position=\"right\"\n                mini\n            >\n                <img\n                    v-if=\"!user.isInit || !user.user.id\"\n                    class=\"rounded-full border border-solid border-gray-200\"\n                    src=\"./../assets/image/avatar.svg\"\n                />\n                <img\n                    v-else\n                    :src=\"user.user.avatar as string\"\n                    class=\"rounded-full border border-solid border-gray-200\"\n                />\n            </a-tooltip>\n            <div v-else>\n                <img\n                    v-if=\"!user.isInit || !user.user.id\"\n                    class=\"rounded-full border border-solid border-gray-200\"\n                    src=\"./../assets/image/avatar.svg\"\n                />\n                <img\n                    v-else\n                    :src=\"user.user.avatar as string\"\n                    class=\"rounded-full border border-solid border-gray-200\"\n                />\n            </div>\n        </div>\n        <div class=\"flex-grow mt-2\">\n            <a\n                class=\"page-nav-item block text-center py-3\"\n                :class=\"activeTab === 'video' ? 'active' : ''\"\n                @click=\"$router.push('/video')\"\n                href=\"javascript:;\"\n            >\n                <div>\n                    <IconAccount class=\"text-xl\" />\n                </div>\n                <div class=\"text-sm\">{{ $t(\"avatar.digitalHuman\") }}</div>\n            </a>\n            <!--            <a class=\"page-nav-item block text-center py-3\"-->\n            <!--               :class=\"activeTab==='sound'?'active':''\"-->\n            <!--               @click=\"$router.push('/sound')\"-->\n            <!--               href=\"javascript:;\">-->\n            <!--                <div>-->\n            <!--                    <icon-sound class=\"text-xl\"/>-->\n            <!--                </div>-->\n            <!--                <div class=\"text-sm\">{{ $t('声音') }}</div>-->\n            <!--            </a>-->\n            <a\n                class=\"page-nav-item block text-center py-3\"\n                :class=\"activeTab === 'server' ? 'active' : ''\"\n                @click=\"$router.push('/server')\"\n                href=\"javascript:;\"\n            >\n                <div>\n                    <IconServer class=\"text-xl\" />\n                </div>\n                <div class=\"text-sm\">{{ $t(\"model.model\") }}</div>\n            </a>\n            <a\n                class=\"page-nav-item block text-center py-3\"\n                :class=\"activeTab === 'setting' ? 'active' : ''\"\n                @click=\"$router.push('/setting')\"\n                href=\"javascript:;\"\n            >\n                <div>\n                    <icon-settings class=\"text-xl\" />\n                </div>\n                <div class=\"text-sm\">{{ $t(\"common.setting\") }}</div>\n            </a>\n        </div>\n        <div></div>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/Setting/SettingAbout.vue",
    "content": "<script setup lang=\"ts\">\nimport { AppConfig } from \"../../config\";\nimport { t } from \"../../lang\";\nimport { useSettingStore } from \"../../store/modules/setting\";\nimport UpdaterButton from \"../common/UpdaterButton.vue\";\n\nconst setting = useSettingStore();\nconst licenseYear = new Date().getFullYear();\n\nconst doOpenLog = async () => {\n    await window.$mapi.app.openPath(window.$mapi.log.root());\n};\n</script>\n\n<template>\n    <div class=\"flex mb-3\">\n        <div class=\"w-20\">{{ t(\"common.version\") }}</div>\n        <div class=\"flex-grow\">\n            <div>\n                v{{ AppConfig.version }} Build {{ setting.buildInfo.buildId }}\n            </div>\n            <div class=\"pt-2\">\n                <UpdaterButton />\n            </div>\n        </div>\n    </div>\n    <div class=\"flex mb-3 items-center\">\n        <div class=\"w-20\">{{ t(\"common.officialSite\") }}</div>\n        <div class=\"flex-grow flex items-center\">\n            <a :href=\"AppConfig.website\" target=\"_blank\" class=\"text-link\">\n                {{ AppConfig.website }}\n            </a>\n            <a\n                :href=\"AppConfig.feedbackUrl\"\n                target=\"_blank\"\n                class=\"align-top arco-btn arco-btn-secondary arco-btn-shape-square arco-btn-size-medium arco-btn-status-normal ml-3\"\n            >\n                <icon-customer-service class=\"mr-1\" />\n                {{ t(\"nav.feedback\") }}\n            </a>\n            <a-button class=\"ml-3\" @click=\"doOpenLog\">\n                <template #icon>\n                    <icon-file />\n                </template>\n                {{ t(\"nav.log\") }}\n            </a-button>\n        </div>\n    </div>\n    <div class=\"flex mb-3 items-center\">\n        <div class=\"w-20\">{{ t(\"about.disclaimer\") }}</div>\n        <div class=\"flex-grow\">\n            {{ t(\"about.license\") }}\n        </div>\n    </div>\n    <div class=\"mb-3\">\n        <a\n            :href=\"AppConfig.websiteGithub\"\n            target=\"_blank\"\n            class=\"bg-gray-100 dark:bg-gray-700 w-48 mr-1 rounded-lg py-2 px-8 inline-flex items-center mb-3 hover:shadow-lg\"\n        >\n            <img src=\"./../../assets/image/github.svg\" class=\"w-12 h-12 mr-2\" />\n            <div class=\"flex-grow\">Github</div>\n        </a>\n        <a\n            :href=\"AppConfig.websiteGitee\"\n            target=\"_blank\"\n            class=\"bg-gray-100 dark:bg-gray-700 w-48 mr-1 rounded-lg py-2 px-8 inline-flex items-center hover:shadow-lg\"\n        >\n            <img src=\"./../../assets/image/gitee.svg\" class=\"w-12 h-12 mr-2\" />\n            <div class=\"flex-grow\">Gitee</div>\n        </a>\n    </div>\n    <div class=\"text-gray-400\">\n        &copy; {{ licenseYear }} {{ AppConfig.title }}\n    </div>\n</template>\n"
  },
  {
    "path": "src/components/Setting/SettingBasic.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport { changeLocale, getLocale, listLocales, t } from \"../../lang\";\nimport { useSettingStore } from \"../../store/modules/setting\";\n\nconst locale = ref(\"\");\n\nonMounted(async () => {\n    locale.value = await getLocale();\n});\n\nconst setting = useSettingStore();\nconst locales = ref(listLocales());\nconst onLocaleChange = (value: string) => {\n    changeLocale(value);\n    locale.value = value as any;\n};\n</script>\n\n<template>\n    <a-form :model=\"{}\" layout=\"vertical\">\n        <a-form-item field=\"name\" :label=\"t('common.language')\">\n            <a-select\n                :model-value=\"locale as string\"\n                @change=\"onLocaleChange as any\"\n            >\n                <a-option\n                    v-for=\"(l, lIndex) in locales\"\n                    :key=\"l.name\"\n                    :value=\"l.name\"\n                    >{{ l.label }}\n                </a-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item field=\"name\" :label=\"t('setting.themeStyle')\">\n            <a-radio-group\n                :model-value=\"setting.configGet('darkMode').value\"\n                @change=\"setting.onConfigChange('darkMode', $event)\"\n            >\n                <a-radio value=\"light\">{{ t(\"theme.light\") }}</a-radio>\n                <a-radio value=\"dark\">{{ t(\"theme.dark\") }}</a-radio>\n                <a-radio value=\"auto\">{{ t(\"setting.followSystem\") }}</a-radio>\n            </a-radio-group>\n        </a-form-item>\n        <a-form-item field=\"name\" :label=\"t('setting.onClose')\">\n            <a-radio-group\n                :model-value=\"setting.configGet('exitMode').value\"\n                @change=\"setting.onConfigChange('exitMode', $event)\"\n            >\n                <a-radio value=\"exit\">{{ t(\"setting.exitDirectly\") }}</a-radio>\n                <a-radio value=\"hide\">{{ t(\"common.hideWindow\") }}</a-radio>\n                <a-radio value=\"\">{{ t(\"setting.askEveryTime\") }}</a-radio>\n            </a-radio-group>\n        </a-form-item>\n    </a-form>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/Setting/SettingEnv.vue",
    "content": "<script setup lang=\"ts\">\nimport SettingEnvHubRoot from \"./components/SettingEnvHubRoot.vue\";\n</script>\n\n<template>\n    <a-form :model=\"null as any\" layout=\"vertical\">\n        <SettingEnvHubRoot />\n    </a-form>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/Setting/components/SettingEnvHubRoot.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport { t } from \"../../../lang\";\nimport { Dialog } from \"../../../lib/dialog\";\n\nconst env = ref({\n    hubRoot: null as string | null,\n    hubRootDefault: null as string | null,\n});\nconst doLoad = async () => {\n    env.value.hubRoot = await window.$mapi.config.get(\"hubRoot\", \"\");\n    env.value.hubRootDefault = await window.$mapi.file.hubRootDefault();\n};\n\nonMounted(doLoad);\n\nconst doSelectHubRootPath = async (useDefault: boolean) => {\n    let dir;\n    if (useDefault) {\n        dir = await window.$mapi.file.hubRootDefault();\n    } else {\n        dir = await window.$mapi.file.openDirectory();\n    }\n    if (dir) {\n        Dialog.confirm(t(\"setting.pathChangeConfirm\", { path: dir })).then(\n            () => {\n                window.$mapi.config.set(\"hubRoot\", dir).then(() => {\n                    window.$mapi.app.quit();\n                });\n            },\n        );\n    }\n};\n\nconst doOpen = () => {\n    window.$mapi.app.openPath(\n        env.value.hubRoot || env.value.hubRootDefault || \"\",\n    );\n};\n</script>\n\n<template>\n    <a-form-item field=\"name\" :label=\"t('setting.storagePath')\">\n        <a-input\n            readonly\n            :placeholder=\"env.hubRootDefault as string\"\n            v-model=\"env.hubRoot as string\"\n        >\n            <template #append>\n                <div\n                    @click=\"doSelectHubRootPath(false)\"\n                    class=\"cursor-pointer pl-3\"\n                >\n                    {{ t(\"common.selectPath\") }}\n                </div>\n            </template>\n        </a-input>\n        <template #help>\n            <div class=\"flex items-center mt-2\">\n                <a-button\n                    size=\"mini\"\n                    class=\"mr-2\"\n                    @click=\"doSelectHubRootPath(true)\"\n                    v-if=\"env.hubRoot && env.hubRoot !== env.hubRootDefault\"\n                >\n                    {{ t(\"common.restoreDefault\") }}\n                </a-button>\n                <a-button size=\"mini\" class=\"mr-2\" @click=\"doOpen()\">\n                    {{ t(\"common.openPath\") }}\n                </a-button>\n                <div>\n                    {{ t(\"setting.pathChangeRestart\") }}\n                </div>\n            </div>\n        </template>\n    </a-form-item>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/TextTruncateView.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { doCopy } from \"./common/util\";\n\nconst props = defineProps({\n    text: {\n        type: String,\n        required: true,\n    },\n    autoTruncate: {\n        type: Boolean,\n        default: true,\n    },\n    maxLength: {\n        type: Number,\n        default: 100,\n    },\n    copyable: {\n        type: Boolean,\n        default: true,\n    },\n});\nconst isTruncate = ref(props.autoTruncate);\nconst showToggle = computed(() => props.text.length > props.maxLength);\nconst displayText = computed(() => {\n    if (isTruncate.value && showToggle.value) {\n        return props.text.slice(0, props.maxLength) + \"...\";\n    }\n    return props.text;\n});\n\nconst handleToggle = () => {\n    isTruncate.value = !isTruncate.value;\n};\n\nconst handleCopy = async () => {\n    await doCopy(props.text);\n};\n</script>\n\n<template>\n    <div :class=\"{ 'cursor-pointer': copyable !== false }\">\n        {{ displayText }}\n        <a-tooltip\n            :content=\"isTruncate ? $t('common.more') : $t('common.collapse')\"\n            mini\n        >\n            <a-button size=\"mini\" v-if=\"showToggle\" @click=\"handleToggle\">\n                <icon-double-down v-if=\"isTruncate\" />\n                <icon-double-up v-else />\n            </a-button>\n        </a-tooltip>\n        <a-tooltip v-if=\"copyable\" :content=\"$t('common.clickToCopy')\" mini>\n            <a-button size=\"mini\" @click=\"handleCopy\">\n                <icon-copy />\n            </a-button>\n        </a-tooltip>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/AudioPlayer.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n    computed,\n    onBeforeUnmount,\n    onMounted,\n    ref,\n    watch,\n    watchEffect,\n} from \"vue\";\nimport WaveSurfer from \"wavesurfer.js\";\nimport RecordPlugin from \"wavesurfer.js/dist/plugins/record.esm.js\";\nimport RegionsPlugin from \"wavesurfer.js/dist/plugins/regions.esm.js\";\nimport { AudioUtil } from \"../../lib/audio\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { TimeUtil } from \"../../lib/util\";\nimport IconRefresh from \"~icons/mdi/refresh\";\nimport IconContentCut from \"~icons/mdi/content-cut\";\nimport IconMicrophone from \"~icons/mdi/microphone\";\n\nconst props = withDefaults(\n    defineProps<{\n        url?: string;\n        recordEnable?: boolean;\n        trimEnable?: boolean;\n        downloadEnable?: boolean;\n        showWave?: boolean;\n    }>(),\n    {\n        url: \"\",\n        recordEnable: false,\n        trimEnable: false,\n        downloadEnable: false,\n        showWave: false,\n    },\n);\n\n// 波形相关\nconst wave = ref<WaveSurfer | null>(null);\nconst waveContainer = ref(null);\nconst waveUrl = ref<string | null>(null);\nconst waveUrlSource = ref<\"url\" | \"trim\" | \"record\" | null>(null);\nconst waveVisible = ref(false);\nconst waveLoadAutoPlay = ref(false);\nconst waveIsLoaded = ref(false);\nconst waveRecord = ref<any>(null);\n\nconst trimUrl = ref<string | null>(null);\n\nconst isPlaying = ref(false);\nconst isTrimming = ref(false);\n\nconst recordUrl = ref<string | null>(null);\nconst isRecording = ref(false);\nconst recordInputDeviceSelect = ref(null);\nconst recordInputDevices = ref<{ id: string; name: string }[]>([]);\nconst recordVisible = ref(false);\n\nconst timeTotal = ref<number>(0);\nconst timeCurrent = ref<number>(0);\nlet regions = RegionsPlugin.create();\nregions.enableDragSelection({\n    color: \"rgba(255, 0, 0, 0.1)\",\n});\n\nconst timeTotalSecond = computed(() => {\n    return Math.round(timeTotal.value);\n});\nconst timeCurrentSecond = computed(() => {\n    return Math.round(timeCurrent.value);\n});\nconst timeTotalFormat = computed(() => {\n    return TimeUtil.secondsToTime(timeTotalSecond.value);\n});\nconst timeCurrentFormat = computed(() => {\n    return TimeUtil.secondsToTime(timeCurrentSecond.value);\n});\nconst isAudioEmpty = computed(() => {\n    return !props.url && !trimUrl.value && !recordUrl.value;\n});\nconst debugInfo = computed(() => {\n    return {\n        waveUrl: waveUrl.value,\n        waveUrlSource: waveUrlSource.value,\n        waveIsLoaded: waveIsLoaded.value,\n    };\n});\n\nonMounted(() => {\n    wave.value = WaveSurfer.create({\n        container: waveContainer.value as any,\n        waveColor: \"#4A90E2\",\n        progressColor: \"#FF5733\",\n        cursorColor: \"#333\",\n        barWidth: 2,\n        height: 40,\n        plugins: [regions],\n        autoplay: false,\n        cursorWidth: 0,\n        sampleRate: 16000,\n    });\n    waveRecord.value = wave.value.registerPlugin(\n        RecordPlugin.create({\n            scrollingWaveform: false,\n            renderRecordedAudio: false,\n        }),\n    );\n    waveRecord.value.on(\"record-end\", (blob) => {\n        recordUrl.value = URL.createObjectURL(blob);\n    });\n    wave.value.on(\"play\", () => {\n        isPlaying.value = true;\n        waveVisible.value = true;\n    });\n    wave.value.on(\"pause\", () => {\n        isPlaying.value = false;\n    });\n    wave.value.on(\"finish\", () => {\n        isPlaying.value = false;\n    });\n    wave.value.on(\"interaction\", () => {\n        wave.value?.play();\n    });\n    wave.value.on(\"ready\", () => {\n        if (isAudioEmpty.value) {\n            return;\n        }\n        waveIsLoaded.value = true;\n        timeTotal.value = wave.value?.getDuration() as number;\n        if (waveLoadAutoPlay.value) {\n            wave.value?.play();\n            waveLoadAutoPlay.value = false;\n        }\n    });\n    wave.value.on(\"timeupdate\", () => {\n        timeCurrent.value = wave.value?.getCurrentTime() as number;\n    });\n    if (props.recordEnable) {\n        if (!props.url) {\n            recordVisible.value = true;\n        }\n        RecordPlugin.getAvailableAudioDevices().then((devices) => {\n            recordInputDevices.value = devices.map((device) => {\n                return {\n                    id: device.deviceId,\n                    name: device.label || device.deviceI,\n                };\n            });\n            if (!recordInputDeviceSelect.value) {\n                if (devices.length > 0) {\n                    recordInputDeviceSelect.value = devices[0].deviceId;\n                }\n            }\n        });\n    }\n});\n\nonBeforeUnmount(() => {\n    if (wave.value) {\n        wave.value.destroy();\n    }\n});\n\nwatch(\n    () => props.url,\n    (url) => {\n        if (url) {\n            // auto add file:// when url is local file\n            if (\n                url.startsWith(\"file:\") ||\n                url.startsWith(\"http:\") ||\n                url.startsWith(\"https:\")\n            ) {\n            } else {\n                url = `file://${url}`;\n            }\n            waveUrl.value = url;\n            waveUrlSource.value = \"url\";\n        }\n    },\n    {\n        immediate: true,\n    },\n);\nwatch(\n    () => trimUrl.value,\n    (url) => {\n        if (url) {\n            waveUrl.value = url;\n            waveUrlSource.value = \"trim\";\n        }\n    },\n    {\n        immediate: true,\n    },\n);\nwatch(\n    () => recordUrl.value,\n    (url) => {\n        if (url) {\n            waveUrl.value = url;\n            waveUrlSource.value = \"record\";\n        }\n    },\n    {\n        immediate: true,\n    },\n);\nwatchEffect(() => {\n    if (wave.value && waveUrl.value) {\n        waveIsLoaded.value = false;\n        wave.value.load(waveUrl.value);\n    }\n});\n\nconst doPlay = () => {\n    if (!waveUrl.value) {\n        return;\n    }\n    if (!waveIsLoaded.value) {\n        waveLoadAutoPlay.value = true;\n        wave.value?.load(waveUrl.value);\n        return;\n    }\n    wave.value?.play();\n};\nconst doPause = () => {\n    wave.value?.pause();\n};\nconst onSeek = (value: number) => {\n    wave.value?.seekTo(value / timeTotal.value);\n};\n\nconst doTrimSave = async () => {\n    if (!isTrimming.value) {\n        return;\n    }\n    const region = regions.getRegions()[0];\n    const buffer = AudioUtil.audioBufferCut(\n        wave.value?.getDecodedData() as AudioBuffer,\n        region.start,\n        region.end,\n    );\n    isTrimming.value = false;\n    regions.clearRegions();\n    wave.value?.empty();\n    trimUrl.value = URL.createObjectURL(AudioUtil.audioBufferToWavBlob(buffer));\n};\n\nconst doTrim = async () => {\n    if (!waveUrl.value) {\n        return;\n    }\n    let start = 1;\n    let end = timeTotal.value - 1;\n    if (end <= start) {\n        start = 0;\n        end = timeTotal.value;\n    }\n    regions.clearRegions();\n    regions.addRegion({\n        start,\n        end,\n        color: \"rgba(255, 255, 0, 0.1)\",\n        drag: true,\n        resize: true,\n    });\n    isTrimming.value = true;\n    waveVisible.value = true;\n};\n\nconst doDownload = () => {\n    if (!waveUrl.value) {\n        return;\n    }\n    const a = document.createElement(\"a\");\n    a.href = waveUrl.value;\n    a.download = \"audio.wav\";\n    a.click();\n};\n\nconst doRecord = () => {\n    recordVisible.value = true;\n};\n\nconst doRecordStart = async () => {\n    if (waveRecord.value.isRecording() || waveRecord.value.isPaused()) {\n        waveRecord.value.stopRecording();\n        return;\n    }\n    try {\n        await waveRecord.value.startRecording({\n            deviceId: recordInputDeviceSelect.value,\n        });\n        isRecording.value = true;\n        waveVisible.value = true;\n    } catch (e) {\n        Dialog.tipError(`${e}`);\n    }\n};\n\nconst doRecordStop = async () => {\n    if (waveRecord.value.isRecording() || waveRecord.value.isPaused()) {\n        await waveRecord.value.stopRecording();\n        isRecording.value = false;\n    }\n    recordVisible.value = false;\n};\n\nconst doRecordBack = async () => {\n    recordVisible.value = false;\n};\n\nconst doRecordClean = async () => {\n    recordVisible.value = true;\n};\n\nconst setRecordFromFile = async (file: File) => {\n    recordUrl.value = URL.createObjectURL(file);\n    recordVisible.value = false;\n};\n\nconst getAudioBuffer = () => {\n    return wave.value?.getDecodedData() as AudioBuffer;\n};\n\ndefineExpose({\n    setRecordFromFile,\n    getAudioBuffer,\n});\n</script>\n\n<template>\n    <div class=\"border rounded-lg py-2\">\n        <pre v-if=\"0\" style=\"white-space: wrap; font-size: 10px\">{{\n            JSON.stringify(debugInfo, null, 2)\n        }}</pre>\n        <div\n            class=\"px-2 overflow-hidden\"\n            :style=\"\n                (isRecording || isTrimming || (showWave && !recordVisible)) &&\n                waveVisible\n                    ? 'height:40px;'\n                    : 'height:0;'\n            \"\n        >\n            <div\n                ref=\"waveContainer\"\n                style=\"height: 40px\"\n                class=\"w-full overflow-hidden\"\n            ></div>\n        </div>\n        <div\n            v-if=\"!recordVisible && waveUrl\"\n            class=\"h-10 px-2 flex items-center\"\n        >\n            <div>\n                <div\n                    v-if=\"!isPlaying\"\n                    @click=\"doPlay\"\n                    class=\"cursor-pointer w-8 h-8 inline-flex\"\n                >\n                    <icon-play-circle\n                        class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                    />\n                </div>\n                <div\n                    v-if=\"isPlaying\"\n                    @click=\"doPause\"\n                    class=\"cursor-pointer w-8 h-8 inline-flex\"\n                >\n                    <icon-pause-circle\n                        class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                    />\n                </div>\n            </div>\n            <div class=\"ml-3 text-gray-500 w-24 text-sm font-mono\">\n                {{ timeCurrentFormat + \"/\" + timeTotalFormat }}\n            </div>\n            <div class=\"ml-3 flex-grow\">\n                <a-slider\n                    :model-value=\"timeCurrent\"\n                    :max=\"timeTotal\"\n                    @change=\"onSeek as any\"\n                    :show-tooltip=\"false\"\n                    :step=\"0.001\"\n                    :min=\"0\"\n                />\n            </div>\n            <div class=\"ml-3\">\n                <a-tooltip\n                    :content=\"$t('common.collapse')\"\n                    mini\n                    v-if=\"\n                        showWave && waveVisible && !isTrimming && !isRecording\n                    \"\n                >\n                    <div\n                        @click=\"waveVisible = false\"\n                        class=\"cursor-pointer w-8 h-8 inline-flex\"\n                    >\n                        <icon-up\n                            class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                        />\n                    </div>\n                </a-tooltip>\n                <a-tooltip\n                    :content=\"$t('voice.rerecord')\"\n                    mini\n                    v-if=\"recordUrl && !isTrimming\"\n                >\n                    <div\n                        @click=\"doRecordClean\"\n                        class=\"cursor-pointer w-8 h-8 inline-flex\"\n                    >\n                        <IconRefresh\n                            class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                        />\n                    </div>\n                </a-tooltip>\n                <a-tooltip\n                    :content=\"$t('media.cropAudio')\"\n                    mini\n                    v-if=\"!isTrimming && props.trimEnable\"\n                >\n                    <div\n                        @click=\"doTrim\"\n                        class=\"cursor-pointer w-8 h-8 inline-flex\"\n                    >\n                        <IconContentCut\n                            class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                        />\n                    </div>\n                </a-tooltip>\n                <a-tooltip\n                    :content=\"$t('media.cropConfirm')\"\n                    mini\n                    v-if=\"isTrimming && props.trimEnable\"\n                >\n                    <div\n                        @click=\"doTrimSave\"\n                        class=\"cursor-pointer w-8 h-8 inline-flex\"\n                    >\n                        <icon-check\n                            class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                        />\n                    </div>\n                </a-tooltip>\n                <a-tooltip\n                    :content=\"$t('download.audio')\"\n                    mini\n                    v-if=\"!isTrimming && props.downloadEnable\"\n                >\n                    <div\n                        @click=\"doDownload\"\n                        class=\"cursor-pointer w-8 h-8 inline-flex\"\n                    >\n                        <icon-download\n                            class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                        />\n                    </div>\n                </a-tooltip>\n                <a-tooltip\n                    :content=\"$t('voice.record')\"\n                    mini\n                    v-if=\"props.recordEnable && !isTrimming && !recordUrl\"\n                >\n                    <div\n                        @click=\"doRecord\"\n                        class=\"cursor-pointer w-8 h-8 inline-flex\"\n                    >\n                        <IconMicrophone\n                            class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                        />\n                    </div>\n                </a-tooltip>\n            </div>\n        </div>\n        <div\n            v-if=\"recordEnable && recordVisible\"\n            class=\"h-10 px-2 flex items-center\"\n        >\n            <div>\n                <div\n                    v-if=\"!isRecording && waveUrl\"\n                    @click=\"doRecordBack\"\n                    class=\"cursor-pointer w-8 h-8 inline-flex\"\n                >\n                    <icon-left\n                        class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                    />\n                </div>\n                <div\n                    v-if=\"recordInputDevices.length && !isRecording\"\n                    @click=\"doRecordStart\"\n                    class=\"cursor-pointer w-8 h-8 inline-flex\"\n                >\n                    <icon-record\n                        class=\"m-auto text-red-700 hover:text-primary text-2xl\"\n                    />\n                </div>\n                <div\n                    v-else-if=\"recordInputDevices.length\"\n                    @click=\"doRecordStop\"\n                    class=\"cursor-pointer w-8 h-8 inline-flex\"\n                >\n                    <icon-record-stop\n                        class=\"m-auto text-gray-700 hover:text-primary text-2xl\"\n                    />\n                </div>\n            </div>\n            <div class=\"ml-3\">\n                <div\n                    v-if=\"!recordInputDevices.length\"\n                    class=\"text-sm bg-gray-100 h-10 leading-10 rounded-lg px-5\"\n                >\n                    <icon-info-circle />\n                    {{ $t(\"error.noMicrophone\") }}\n                </div>\n                <a-select\n                    v-else\n                    v-model=\"recordInputDeviceSelect as any\"\n                    size=\"mini\"\n                    style=\"width: 100%\"\n                >\n                    <a-option\n                        v-for=\"device in recordInputDevices\"\n                        :key=\"device.id\"\n                        :value=\"device.id\"\n                    >\n                        {{ device.name }}\n                    </a-option>\n                </a-select>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/CodeViewer.vue",
    "content": "<script setup lang=\"ts\">\nimport { PropType, ref, watch } from \"vue\";\nimport { EditorView, keymap, lineNumbers } from \"@codemirror/view\";\nimport { dracula } from \"@uiw/codemirror-theme-dracula\";\nimport { quietlight } from \"@uiw/codemirror-theme-quietlight\";\nimport { python } from \"@codemirror/lang-python\";\nimport { json } from \"@codemirror/lang-json\";\nimport { defaultKeymap } from \"@codemirror/commands\";\nimport { EditorState } from \"@codemirror/state\";\nimport debounce from \"lodash/debounce\";\n\nconst props = defineProps({\n    lang: {\n        type: String as PropType<\"text\" | \"python\" | \"json\">,\n        default: \"python\",\n    },\n    code: {\n        type: String,\n        default: \"\",\n    },\n    dark: {\n        type: Boolean,\n        default: false,\n    },\n});\nconst codeEditorDom = ref<HTMLElement>();\nlet editor = null as EditorView | null;\n\nconst showDebounce = debounce(async () => {\n    const extentions = [\n        props.dark ? dracula : quietlight,\n        keymap.of(defaultKeymap),\n        lineNumbers(),\n        EditorState.readOnly.of(true),\n    ];\n    switch (props.lang) {\n        case \"text\":\n            break;\n        case \"python\":\n            extentions.push(python());\n            break;\n        case \"json\":\n            extentions.push(json());\n            break;\n    }\n    if (editor) {\n        editor.dispatch({\n            changes: {\n                from: 0,\n                to: editor.state.doc.length,\n                insert: props.code,\n            },\n        });\n    } else {\n        editor = new EditorView({\n            state: EditorState.create({\n                doc: props.code,\n                extensions: extentions,\n            }),\n            parent: codeEditorDom.value,\n        });\n    }\n}, 100);\n\nwatch(() => props.code, showDebounce);\nwatch(() => props.lang, showDebounce);\n</script>\n\n<template>\n    <div class=\"w-full h-96\">\n        <div ref=\"codeEditorDom\" class=\"\"></div>\n    </div>\n</template>\n\n<style lang=\"less\">\n.cm-editor {\n    height: 100%;\n    font-size: 0.8rem;\n}\n</style>\n"
  },
  {
    "path": "src/components/common/CodeViewerDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref } from \"vue\";\nimport { EditorView, keymap, lineNumbers } from \"@codemirror/view\";\nimport { dracula } from \"@uiw/codemirror-theme-dracula\";\nimport { quietlight } from \"@uiw/codemirror-theme-quietlight\";\nimport { python } from \"@codemirror/lang-python\";\nimport { defaultKeymap } from \"@codemirror/commands\";\nimport { EditorState } from \"@codemirror/state\";\n\nconst visible = ref(false);\nconst codeEditorDom = ref<HTMLElement>();\nlet editor = null as EditorView | null;\nconst useDark = false;\n\nconst show = (code: string) => {\n    visible.value = true;\n    nextTick(() => {\n        initEditor();\n        setEditorContent(code);\n    });\n};\n\nconst initEditor = () => {\n    if (editor) {\n        return;\n    }\n    editor = new EditorView({\n        extensions: [\n            useDark ? dracula : quietlight,\n            python(),\n            keymap.of(defaultKeymap),\n            lineNumbers(),\n            EditorState.readOnly.of(true),\n        ],\n        parent: codeEditorDom.value,\n    });\n};\n\nconst setEditorContent = (code: string) => {\n    if (!editor) {\n        setTimeout(() => {\n            setEditorContent(code);\n        }, 100);\n        return;\n    }\n    const transaction = editor.state.update({\n        changes: { from: 0, to: editor.state.doc.length, insert: code },\n    });\n    editor.dispatch(transaction);\n};\n\nonMounted(() => {});\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal v-model:visible=\"visible\" :footer=\"false\" width=\"80vw\">\n        <template #title>\n            {{ $t(\"common.viewCode\") }}\n        </template>\n        <div>\n            <div class=\"w-full h-96\">\n                <div ref=\"codeEditorDom\" class=\"\"></div>\n            </div>\n        </div>\n    </a-modal>\n</template>\n\n<style lang=\"less\">\n.cm-editor {\n    height: 100%;\n    font-size: 1.2rem;\n}\n</style>\n"
  },
  {
    "path": "src/components/common/DataConfigDialogButton.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { doCopy } from \"./util\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { t } from \"../../lang\";\n\nconst props = defineProps<{\n    size?: \"small\" | undefined;\n    title: string;\n    name: string;\n    defaultValue: string | { key: string; value: string }[];\n    placeholder?: string;\n    help?: string;\n    param?: Record<string, string> | { name: string; label: string }[];\n}>();\nconst visible = ref(false);\nconst content = ref<string | { key: string; value: string }[]>(\"\");\nconst doShow = async () => {\n    visible.value = true;\n    content.value = (await $mapi.storage.get(\n        \"data\",\n        props.name,\n        props.defaultValue,\n    )) as string;\n};\nconst doSave = () => {\n    visible.value = false;\n    $mapi.storage.set(\"data\", props.name, content.value);\n    Dialog.tipSuccess(t(\"common.saveSuccess\"));\n};\nconst doRestore = () => {\n    content.value = props.defaultValue;\n    $mapi.storage.set(\"data\", props.name, content.value);\n};\nconst type = computed(() => {\n    if (Array.isArray(props.defaultValue)) {\n        return \"keyValueList\";\n    }\n    return \"text\";\n});\n</script>\n\n<template>\n    <a-button @click=\"doShow()\" :size=\"size\">\n        <template #icon>\n            <icon-settings />\n        </template>\n        {{ title }}\n    </a-button>\n    <a-modal v-model:visible=\"visible\" width=\"800px\" title-align=\"start\">\n        <template #title>\n            {{ title }}\n        </template>\n        <template #footer>\n            <a-button @click=\"doRestore\">\n                {{ $t(\"common.restoreDefault\") }}\n            </a-button>\n            <a-button type=\"primary\" @click=\"doSave\">\n                {{ $t(\"common.save\") }}\n            </a-button>\n        </template>\n        <div class=\"-mx-2 -my-3\" style=\"height: 60vh\">\n            <slot></slot>\n            <div v-if=\"help\">\n                <a-alert type=\"info\" show-icon class=\"mb-2\">\n                    {{ help }}\n                </a-alert>\n            </div>\n            <div v-if=\"type === 'keyValueList'\">\n                <div\n                    v-for=\"(item, index) in content as any\"\n                    :key=\"index\"\n                    class=\"mb-2 flex items-center\"\n                >\n                    <a-input\n                        v-model=\"item.key\"\n                        :placeholder=\"$t('common.key')\"\n                        class=\"mr-2\"\n                    />\n                    <a-input\n                        v-model=\"item.value\"\n                        :placeholder=\"$t('common.value')\"\n                        class=\"mr-2\"\n                    />\n                    <a-button\n                        type=\"text\"\n                        danger\n                        @click=\"(content as any).splice(index, 1)\"\n                    >\n                        <icon-delete />\n                    </a-button>\n                </div>\n                <div>\n                    <a-button\n                        type=\"dashed\"\n                        block\n                        @click=\"(content as any).push({ key: '', value: '' })\"\n                    >\n                        <template #icon>\n                            <icon-plus />\n                        </template>\n                        {{ $t(\"common.add\") }}\n                    </a-button>\n                </div>\n            </div>\n            <div v-else>\n                <a-textarea\n                    v-model=\"content as any\"\n                    :placeholder=\"placeholder\"\n                    :auto-size=\"{ minRows: 15, maxRows: 15 }\"\n                />\n            </div>\n            <div v-if=\"props.param\">\n                <div class=\"mt-2 font-bold\">\n                    {{ $t(\"common.availableVars\") }}:\n                </div>\n                <div class=\"mt-1\" v-if=\"Array.isArray(props.param)\">\n                    <div\n                        v-for=\"item in props.param as any\"\n                        :key=\"item.name\"\n                        class=\"mr-4 inline-flex items-center text-xs\"\n                    >\n                        <div\n                            class=\"font-mono mr-1 cursor-pointer\"\n                            @click=\"doCopy(`{${item.name}}`)\"\n                        >\n                            {{ \"{\" + item.name + \"}\" }}\n                        </div>\n                        <div class=\"text-gray-400\">\n                            {{ item.label }}\n                        </div>\n                    </div>\n                </div>\n                <div class=\"mt-1\" v-else>\n                    <div\n                        v-for=\"(value, key) in props.param\"\n                        :key=\"key\"\n                        class=\"mr-4 inline-flex items-center text-xs\"\n                    >\n                        <div\n                            class=\"font-mono mr-1 cursor-pointer\"\n                            @click=\"doCopy(`{${key}}`)\"\n                        >\n                            {{ \"{\" + key + \"}\" }}\n                        </div>\n                        <div class=\"text-gray-400\">\n                            {{ value }}\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"h-4\"></div>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/components/common/DragPasteContainer.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from \"vue\";\nimport { UI } from \"../../lib/ui\";\n\ntype DragPasteFile = {\n    name: string;\n    isDirectory: boolean;\n    isFile: boolean;\n    path: string;\n    fileExt: string;\n};\n\nconst dragPasteContainer = ref<HTMLElement | null>(null);\nconst props = defineProps({\n    pasteEnable: {\n        type: Boolean,\n        default: true,\n    },\n    dragEnable: {\n        type: Boolean,\n        default: true,\n    },\n});\nconst emit = defineEmits({\n    input: (files: DragPasteFile[]) => true,\n});\nonMounted(() => {\n    if (props.pasteEnable) {\n        window.addEventListener(\"paste\", onPaste);\n    }\n    if (props.dragEnable) {\n        dragPasteContainer.value?.addEventListener(\"dragenter\", onDragEnter);\n        dragPasteContainer.value?.addEventListener(\"dragstart\", onDragStart);\n        dragPasteContainer.value?.addEventListener(\"dragend\", onDragEnd);\n        dragPasteContainer.value?.addEventListener(\"dragover\", onDragOver);\n        dragPasteContainer.value?.addEventListener(\"dragleave\", onDragLeave);\n        dragPasteContainer.value?.addEventListener(\"drop\", onDrop);\n        UI.onResize(dragPasteContainer.value, (width, height) => {\n            const rect = dragPasteContainer.value?.getBoundingClientRect();\n            if (rect) {\n                dragMaskStyle.value = {\n                    left: `${rect.left}px`,\n                    top: `${rect.top}px`,\n                    width: `${rect.width}px`,\n                    height: `${rect.height}px`,\n                };\n            }\n            // console.log('resize', width, height, dragPasteContainer.value?.getBoundingClientRect())\n        });\n    }\n});\nonBeforeUnmount(() => {\n    if (props.pasteEnable) {\n        window.removeEventListener(\"paste\", onPaste);\n    }\n    if (props.dragEnable) {\n        dragPasteContainer.value?.removeEventListener(\"dragenter\", onDragEnter);\n        dragPasteContainer.value?.removeEventListener(\"dragstart\", onDragStart);\n        dragPasteContainer.value?.removeEventListener(\"dragend\", onDragEnd);\n        dragPasteContainer.value?.removeEventListener(\"dragover\", onDragOver);\n        dragPasteContainer.value?.removeEventListener(\"dragleave\", onDragLeave);\n        dragPasteContainer.value?.removeEventListener(\"drop\", onDrop);\n        UI.offResize(dragPasteContainer.value);\n    }\n});\n\nconst dragIsOver = ref(false);\nconst dragMaskStyle = ref({\n    left: \"0\",\n    top: \"0\",\n    width: \"100%\",\n    height: \"100%\",\n});\n\nconst onPaste = async (e: ClipboardEvent) => {\n    const items = e.clipboardData?.items || [];\n    const files: any[] = [];\n    for (let i = 0; i < items.length; i++) {\n        if (items[i].kind === \"file\") {\n            const file = items[i].getAsFile();\n            if (file) {\n                files.push(file);\n            }\n        }\n    }\n    onInput(files).then();\n};\n\nconst onDrop = (e: DragEvent) => {\n    dragIsOver.value = false;\n    // console.log('onDrop')\n    e.preventDefault();\n    e.stopPropagation();\n    const files = e.dataTransfer?.files || [];\n    if (files.length > 0) {\n        onInput(Array.from(files)).then();\n    }\n};\n\nconst onInput = async (files: any[]) => {\n    dragIsOver.value = false;\n    const results: DragPasteFile[] = [];\n    for (const f of files) {\n        const isDirectory = await window.$mapi.file.isDirectory(f.path, {\n            isDataPath: false,\n        });\n        const pcs = f.name.split(\".\");\n        let fileExt = \"\";\n        if (pcs.length > 1) {\n            fileExt = (pcs.pop() || \"\").toLowerCase();\n        }\n        results.push({\n            name: f.name,\n            isDirectory: isDirectory,\n            isFile: !isDirectory,\n            path: f.path,\n            fileExt,\n        });\n    }\n    emit(\"input\", results);\n};\n\nconst onDragEnter = (e: DragEvent) => {\n    // console.log('onDragEnter')\n    e.preventDefault();\n    e.stopPropagation();\n    dragIsOver.value = true;\n};\n\nconst onDragStart = (e: DragEvent) => {\n    // console.log('onDragStart')\n    e.preventDefault();\n    e.stopPropagation();\n    dragIsOver.value = true;\n};\n\nconst onDragEnd = (e: DragEvent) => {\n    // console.log('onDragEnd')\n    e.preventDefault();\n    e.stopPropagation();\n    dragIsOver.value = false;\n};\n\nconst onDragOver = (e: DragEvent) => {\n    // console.log('onDragOver')\n    e.preventDefault();\n    e.stopPropagation();\n    dragIsOver.value = true;\n};\n\nconst onDragLeave = (e: DragEvent) => {\n    // console.log('onDragLeave')\n    e.preventDefault();\n    e.stopPropagation();\n    dragIsOver.value = false;\n};\n</script>\n\n<template>\n    <div ref=\"dragPasteContainer\" class=\"relative\">\n        <slot></slot>\n        <div\n            class=\"fixed bg-white bg-opacity-80 flex debug pointer-events-none\"\n            v-if=\"dragIsOver\"\n            :style=\"dragMaskStyle\"\n        >\n            <div class=\"m-auto text-center text-gray-400\">\n                <div>\n                    <icon-file class=\"text-5xl\" />\n                </div>\n                <div>\n                    <icon-drag-arrow />\n                    {{ $t(\"common.dragHereToRelease\") }}\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/FeedbackTicketButton.vue",
    "content": "<script setup lang=\"ts\">\nimport { AppConfig } from \"../../config\";\n\nconst doShow = () => {\n    window.$mapi.app.windowOpen(\"feedback\");\n};\n</script>\n\n<template>\n    <a-button v-if=\"!!AppConfig.feedbackUrl\" size=\"mini\" @click=\"doShow\">\n        <template #icon>\n            <icon-customer-service />\n        </template>\n        {{ $t(\"nav.feedback\") }}\n    </a-button>\n</template>\n"
  },
  {
    "path": "src/components/common/FileExt.vue",
    "content": "<script setup lang=\"ts\">\nimport ai from \"./FileExtAssets/ai.svg\";\nimport apk from \"./FileExtAssets/apk.svg\";\nimport chm from \"./FileExtAssets/chm.svg\";\nimport css from \"./FileExtAssets/css.svg\";\nimport doc from \"./FileExtAssets/doc.svg\";\nimport docx from \"./FileExtAssets/docx.svg\";\nimport dwg from \"./FileExtAssets/dwg.svg\";\nimport folder from \"./FileExtAssets/folder.svg\";\nimport gif from \"./FileExtAssets/gif.svg\";\nimport html from \"./FileExtAssets/html.svg\";\nimport jpeg from \"./FileExtAssets/jpeg.svg\";\nimport jpg from \"./FileExtAssets/jpg.svg\";\nimport log from \"./FileExtAssets/log.svg\";\nimport mp3 from \"./FileExtAssets/mp3.svg\";\nimport mp4 from \"./FileExtAssets/mp4.svg\";\nimport pdf from \"./FileExtAssets/pdf.svg\";\nimport png from \"./FileExtAssets/png.svg\";\nimport ppt from \"./FileExtAssets/ppt.svg\";\nimport pptx from \"./FileExtAssets/pptx.svg\";\nimport psd from \"./FileExtAssets/psd.svg\";\nimport rar from \"./FileExtAssets/rar.svg\";\nimport svg from \"./FileExtAssets/svg.svg\";\nimport torrent from \"./FileExtAssets/torrent.svg\";\nimport txt from \"./FileExtAssets/txt.svg\";\nimport unknown from \"./FileExtAssets/unknown.svg\";\nimport xls from \"./FileExtAssets/xls.svg\";\nimport xlsx from \"./FileExtAssets/xlsx.svg\";\nimport zip from \"./FileExtAssets/zip.svg\";\nimport fad from \"./FileExtAssets/fad.svg\";\nimport { computed } from \"vue\";\n\nconst images = {\n    ai,\n    apk,\n    chm,\n    css,\n    doc,\n    docx,\n    dwg,\n    folder,\n    gif,\n    html,\n    jpeg,\n    jpg,\n    log,\n    mp3,\n    mp4,\n    pdf,\n    png,\n    ppt,\n    pptx,\n    psd,\n    rar,\n    svg,\n    torrent,\n    txt,\n    unknown,\n    xls,\n    xlsx,\n    zip,\n    fad,\n};\n\nconst props = withDefaults(\n    defineProps<{\n        isFolder?: boolean;\n        name: string;\n        size?: string;\n    }>(),\n    {\n        isFolder: false,\n        size: \"100%\",\n    },\n);\n\nconst extSrc = computed(() => {\n    if (props.isFolder) {\n        return images[\"folder\"];\n    }\n    const ext = props.name.split(\".\").pop()?.toLowerCase();\n    if (ext && images[ext]) {\n        return images[ext];\n    }\n    return images[\"unknown\"];\n});\n\nconst extSrcUrl = computed(() => {\n    return `url(\"${extSrc.value}\")`;\n});\n</script>\n\n<template>\n    <div\n        class=\"pb-file-ext rounded\"\n        :style=\"{\n            width: props.size,\n            height: props.size,\n            backgroundImage: extSrcUrl,\n        }\"\n    ></div>\n</template>\n\n<style scoped>\n.pb-file-ext {\n    display: inline-block;\n    background-size: contain;\n    background-position: center;\n    background-repeat: no-repeat;\n\n    &:after {\n        content: \"\";\n        display: block;\n        padding-top: 100%;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/components/common/FileLogViewer.vue",
    "content": "<script setup lang=\"ts\">\nimport LogViewer from \"./LogViewer.vue\";\nimport { onBeforeUnmount, onMounted, ref } from \"vue\";\n\nconst props = withDefaults(\n    defineProps<{\n        file: string;\n        maxLines?: number;\n        height?: string;\n        autoScroll?: boolean;\n        isDataPath?: boolean;\n    }>(),\n    {\n        file: \"\",\n        maxLines: 1000,\n        height: \"100%\",\n        autoScroll: true,\n        isDataPath: true,\n    },\n);\n\ninterface LogItem {\n    num: number;\n    text: string;\n}\n\nconst logs = ref<LogItem[]>([]);\nlet controller: any = null;\nonMounted(async () => {\n    controller = await window.$mapi.file.watchText(\n        props.file,\n        (data: {}) => {\n            logs.value.push(data as LogItem);\n            while (logs.value.length > props.maxLines) {\n                logs.value.shift();\n            }\n        },\n        {\n            isDataPath: props.isDataPath,\n            limit: props.maxLines,\n        },\n    );\n});\nonBeforeUnmount(() => {\n    if (controller) {\n        controller.stop();\n    }\n});\n</script>\n\n<template>\n    <div :style=\"{ height: props.height }\">\n        <LogViewer\n            :height=\"props.height\"\n            :logs=\"logs\"\n            :auto-scroll=\"autoScroll\"\n        />\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/FilesSelector.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { t } from \"../../lang\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { FileUtil } from \"../../lib/file\";\nimport { doOpenFile } from \"./util\";\n\nconst props = defineProps<{\n    modelValue: string[];\n    extensions: string[];\n}>();\nconst emit = defineEmits<{\n    \"update:modelValue\": [string[]];\n}>();\n\nconst doSelectFile = async () => {\n    const result = await doOpenFile({\n        extensions: props.extensions,\n        multiple: true,\n    });\n    if (!result) {\n        return;\n    }\n    const files = Array.isArray(result) ? result : [result];\n    const validFiles: string[] = [];\n    for (const file of files) {\n        const ext = FileUtil.getExt(file);\n        if (!props.extensions.includes(ext)) {\n            Dialog.tipError(\n                t(\"hint.selectFileFormat\", {\n                    extensions: props.extensions.join(\",\"),\n                }),\n            );\n            return;\n        }\n        validFiles.push(file);\n    }\n    emit(\"update:modelValue\", [...props.modelValue, ...validFiles]);\n};\n\nconst removeFile = (index: number) => {\n    const newValue = [...props.modelValue];\n    newValue.splice(index, 1);\n    emit(\"update:modelValue\", newValue);\n};\n\nconst names = computed(() => {\n    return props.modelValue.map((path) => FileUtil.getBaseName(path, true));\n});\n</script>\n\n<template>\n    <div class=\"flex flex-col gap-2\">\n        <div v-if=\"modelValue.length > 0\" class=\"flex flex-col gap-1\">\n            <div\n                v-for=\"(name, index) in names\"\n                :key=\"index\"\n                class=\"flex items-center gap-2 text-sm text-black rounded-lg leading-7 px-3 min-h-7 border border-gray-500\"\n            >\n                <icon-file />\n                <a-tooltip :content=\"modelValue[index]\" mini>\n                    <span class=\"flex-grow\">{{ name }}</span>\n                </a-tooltip>\n                <a-button size=\"mini\" @click=\"removeFile(index)\">\n                    <icon-close />\n                </a-button>\n            </div>\n        </div>\n        <a-button @click=\"doSelectFile\">\n            <icon-plus />\n            {{ t(\"common.addFile\") }}\n            ({{\n                t(\"common.extensions\", { extensions: extensions.join(\", \") })\n            }})\n        </a-button>\n    </div>\n</template>\n"
  },
  {
    "path": "src/components/common/HtmlViewer.vue",
    "content": "<script setup lang=\"ts\">\nconst props = defineProps<{\n    value: string | undefined;\n}>();\n</script>\n\n<template>\n    <div class=\"pb-html-viewer\" v-html=\"props.value\"></div>\n</template>\n\n<style lang=\"less\" scoped>\n.pb-html-viewer {\n}\n</style>\n"
  },
  {
    "path": "src/components/common/InputInlineEditor.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick, ref } from \"vue\";\n\nconst props = defineProps<{\n    value: string | null | undefined;\n    onChangeCallback?: (value: string) => Promise<boolean>;\n}>();\nconst emit = defineEmits({\n    change: (value: string) => true,\n});\nconst visible = ref(false);\nconst valueEdit = ref(\"\");\nconst onVisibleChange = (visible: boolean) => {\n    if (visible) {\n        valueEdit.value = props.value as string;\n    }\n};\nconst doEnter = () => {\n    nextTick(() => {\n        doConfirm();\n    });\n};\nconst doConfirm = () => {\n    if (props.onChangeCallback) {\n        props.onChangeCallback(valueEdit.value).then((ok) => {\n            if (ok) {\n                visible.value = false;\n                emit(\"change\", valueEdit.value);\n            }\n        });\n    } else {\n        emit(\"change\", valueEdit.value);\n        nextTick(() => {\n            visible.value = false;\n        });\n    }\n};\n</script>\n\n<template>\n    <div>\n        <a-popover\n            v-model:popup-visible=\"visible\"\n            trigger=\"click\"\n            @popup-visible-change=\"onVisibleChange\"\n        >\n            <slot></slot>\n            <template #content>\n                <div class=\"flex\">\n                    <a-input v-model=\"valueEdit\" @pressEnter=\"doEnter\">\n                        <template #append>\n                            <div class=\"cursor-pointer\" @click=\"doConfirm\">\n                                <icon-check />\n                            </div>\n                        </template>\n                    </a-input>\n                </div>\n            </template>\n        </a-popover>\n    </div>\n</template>\n"
  },
  {
    "path": "src/components/common/LogViewer.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick, ref, watch } from \"vue\";\nimport IconInboxOutline from \"~icons/mdi/inbox-outline\";\n\nconst logContainer = ref<HTMLElement | null>(null);\n\ninterface LogItem {\n    num: number;\n    text: string;\n}\n\nconst props = withDefaults(\n    defineProps<{\n        logs: LogItem[];\n        autoScroll?: boolean;\n        height?: string;\n    }>(),\n    {\n        logs: [] as any,\n        autoScroll: true,\n        height: \"100%\",\n    },\n);\n\nconst scrollToBottom = () => {\n    nextTick(() => {\n        logContainer.value?.scrollTo({\n            top: logContainer.value?.scrollHeight,\n            behavior: \"smooth\",\n        });\n    });\n};\n\nwatch(\n    () => {\n        return props.logs;\n    },\n    () => {\n        if (props.autoScroll) {\n            scrollToBottom();\n        }\n    },\n    {\n        immediate: true,\n        deep: true,\n    },\n);\n\nwatch(\n    () => {\n        return props.autoScroll;\n    },\n    () => {\n        if (props.autoScroll) {\n            scrollToBottom();\n        }\n    },\n);\n</script>\n\n<template>\n    <div\n        :style=\"{ height: props.height }\"\n        ref=\"logContainer\"\n        class=\"bg-black p-3 overflow-auto\"\n    >\n        <div v-if=\"!logs.length\" class=\"text-center text-white py-10\">\n            <div>\n                <IconInboxOutline class=\"text-4xl\" />\n            </div>\n            <div class=\"text-xs mt-3\">\n                {{ $t(\"empty.noLog\") }}\n            </div>\n        </div>\n        <div\n            v-for=\"log in logs\"\n            class=\"text-white text-sm font-mono leading-6 whitespace-nowrap\"\n        >\n            <div class=\"inline-block text-right min-w-10 pr-3 text-gray-400\">\n                {{ log.num }}\n            </div>\n            <div class=\"inline-block\">{{ log.text }}</div>\n        </div>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/LogViewerDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport LogViewer from \"./LogViewer.vue\";\nimport FileLogViewer from \"./FileLogViewer.vue\";\n\nconst visible = ref(false);\nconst autoScroll = ref(true);\n\nconst props = withDefaults(\n    defineProps<{\n        logFile?: string | null;\n        logs?: object[] | null;\n        height?: string;\n    }>(),\n    {\n        logFile: null,\n        logs: null,\n        height: \"60vh\",\n    },\n);\n\nconst show = () => {\n    visible.value = true;\n};\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        title-align=\"start\"\n        :footer=\"false\"\n        width=\"80vw\"\n    >\n        <template #title>\n            {{ $t(\"log.view\") }}\n        </template>\n        <div>\n            <div class=\"mb-2 -mt-3\">\n                <a-checkbox v-model=\"autoScroll\">{{\n                    $t(\"log.autoScroll\")\n                }}</a-checkbox>\n            </div>\n            <div class=\"\" v-if=\"visible\">\n                <div v-if=\"logs\">\n                    <LogViewer\n                        :logs=\"logs as any\"\n                        :height=\"height\"\n                        :auto-scroll=\"autoScroll\"\n                    />\n                </div>\n                <div v-else-if=\"logFile\">\n                    <FileLogViewer\n                        :file=\"logFile as string\"\n                        :height=\"height\"\n                        :auto-scroll=\"autoScroll\"\n                    />\n                </div>\n            </div>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/components/common/MEmpty.vue",
    "content": "<template>\n    <div class=\"py-20 text-center text-gray-400 h-full flex\">\n        <div class=\"m-auto\">\n            <div class=\"text-center\">\n                <img\n                    class=\"w-20 w-h-20 object-contain m-auto dark:hidden\"\n                    src=\"./../../assets/image/no-record.svg\"\n                />\n                <img\n                    class=\"w-20 w-h-20 object-contain m-auto hidden dark:block\"\n                    src=\"./../../assets/image/no-record-dark.svg?12\"\n                />\n            </div>\n            <div class=\"text-center text-sm\">\n                {{ text || $t(\"empty.noRecord\") }}\n            </div>\n        </div>\n    </div>\n</template>\n<script setup lang=\"ts\">\nconst props = defineProps<{\n    text?: string;\n}>();\n</script>\n"
  },
  {
    "path": "src/components/common/MLoading.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineProps } from \"vue\";\n\nconst props = defineProps({\n    page: {\n        type: Boolean,\n        default: false,\n    },\n    blocks: {\n        type: Number,\n        default: 8,\n    },\n});\n</script>\n<template>\n    <div class=\"py-10 text-center text-gray-400\">\n        <div v-if=\"props.page\">\n            <div\n                v-for=\"i in props.blocks\"\n                class=\"bg-gray-100 h-10 rounded-lg mb-5\"\n            ></div>\n        </div>\n        <div v-else>\n            <a-spin style=\"--primary-6: #999\" />\n            <div class=\"text-sm\">\n                {{ $t(\"status.loading\") }}\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/components/common/PageWebviewStatus.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\n\ntype StatusType = \"success\" | \"fail\" | \"loading\";\nconst status = ref<StatusType>(\"loading\");\nconst emit = defineEmits([\"refresh\"]);\nconst setStatus = (s: StatusType) => {\n    status.value = s;\n};\ndefineExpose({\n    setStatus,\n});\n</script>\n\n<template>\n    <div\n        v-if=\"'loading' === status\"\n        class=\"absolute inset-0 flex bg-default text-default\"\n    >\n        <div class=\"m-auto text-center text-gray-400\">\n            <div>\n                <icon-loading class=\"text-3xl\" />\n            </div>\n            <div class=\"text-sm pt-2\">{{ $t(\"common.loadingDots\") }}</div>\n        </div>\n    </div>\n    <div\n        v-else-if=\"'fail' === status\"\n        class=\"absolute inset-0 flex bg-default text-default bg-opacity-50\"\n    >\n        <div class=\"m-auto text-center text-red-400\">\n            <div>\n                <icon-info-circle class=\"text-3xl\" />\n            </div>\n            <div>{{ $t(\"common.loadFailedCheckNetwork\") }}</div>\n            <div class=\"mt-4\">\n                <a-button size=\"mini\" @click=\"emit('refresh')\">\n                    <template #icon>\n                        <icon-refresh class=\"tw-mr-1\" />\n                    </template>\n                    {{ $t(\"common.refresh\") }}\n                </a-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped lang=\"less\"></style>\n"
  },
  {
    "path": "src/components/common/ProUpgrade.vue",
    "content": "<script setup lang=\"ts\">\nimport { AppConfig } from \"../../config\";\nimport { t } from \"../../lang\";\n\nconst props = defineProps({\n    desc: {\n        type: String,\n        default: \"\",\n    },\n});\n</script>\n\n<template>\n    <div\n        class=\"pro-upgrade-container h-full w-full flex items-center justify-center p-4\"\n    >\n        <div\n            class=\"text-center py-16 px-12 bg-white rounded-xl shadow-sm border border-gray-100 max-w-md w-full transition-all hover:shadow-md\"\n        >\n            <div class=\"text-center mb-8 relative\">\n                <svg\n                    t=\"1744854394926\"\n                    class=\"mx-auto w-20 h-20\"\n                    viewBox=\"0 0 1024 1024\"\n                    version=\"1.1\"\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    p-id=\"5741\"\n                    xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n                    width=\"256\"\n                    height=\"256\"\n                >\n                    <path\n                        d=\"M1024 512c0 282.784-229.216 512-512 512S0 794.784 0 512 229.216 0 512 0s512 229.216 512 512z\"\n                        fill=\"#F8BC4D\"\n                        p-id=\"5742\"\n                    ></path>\n                    <path\n                        d=\"M598.56 581.504v137.728a32 32 0 0 1-32 32h-105.728a32 32 0 0 1-32-32v-137.728H314.784a28.48 28.48 0 0 1-22.688-45.728l198.752-261.536a28.48 28.48 0 0 1 45.472 0.16l195.68 261.536a28.48 28.48 0 0 1-22.816 45.536h-110.624z\"\n                        fill=\"#FFFFFF\"\n                        p-id=\"5743\"\n                    ></path>\n                </svg>\n            </div>\n            <h3 class=\"text-xl font-bold text-gray-800 mb-3\">\n                {{ $t(\"proUpgrade.title\") }}\n            </h3>\n            <p class=\"text-gray-500 mb-8 leading-relaxed\">\n                {{ desc || $t(\"proUpgrade.defaultDesc\") }}\n            </p>\n            <div>\n                <a\n                    class=\"arco-btn arco-btn-size-large arco-btn-primary px-8 rounded-full shadow-blue-200 shadow-lg hover:shadow-xl transition-all\"\n                    :href=\"AppConfig.website\"\n                    target=\"_blank\"\n                >\n                    <icon-link />\n                    <span class=\"ml-2\">{{\n                        $t(\"proUpgrade.downloadButton\")\n                    }}</span>\n                </a>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped>\n.pro-upgrade-container {\n    background-color: #f8fafc; /* subtle background for the whole area */\n}\n</style>\n"
  },
  {
    "path": "src/components/common/SettingItemYesNo.vue",
    "content": "<script setup lang=\"ts\"></script>\n\n<template>\n    <a-radio-group type=\"button\" size=\"small\">\n        <a-radio value=\"yes\">{{ $t(\"common.yes\") }}</a-radio>\n        <a-radio value=\"no\">{{ $t(\"common.no\") }}</a-radio>\n    </a-radio-group>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/SettingItemYesNoDefault.vue",
    "content": "<script setup lang=\"ts\"></script>\n\n<template>\n    <a-radio-group type=\"button\" size=\"small\">\n        <a-radio value=\"yes\">{{ $t(\"common.yes\") }}</a-radio>\n        <a-radio value=\"no\">{{ $t(\"common.no\") }}</a-radio>\n        <a-radio value=\"\">{{ $t(\"common.default\") }}</a-radio>\n    </a-radio-group>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/TaskBizStatus.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { t } from \"../../lang\";\n\ninterface Props {\n    status: string | undefined;\n    statusMsg: string | undefined;\n}\n\nconst props = defineProps<Props>();\n\nconst statusColor = computed(() => {\n    const colorMap = {\n        queue: \"bg-gray-400\",\n        wait: \"bg-gray-400\",\n        running: \"bg-yellow-500\",\n        success: \"bg-green-500\",\n        fail: \"bg-red-500\",\n    };\n    return colorMap[props.status as string] || \"bg-gray-400\";\n});\n\nconst statusText = computed(() => {\n    const textMap = {\n        queue: t(\"status.queuing\"),\n        wait: t(\"status.waiting\"),\n        running: t(\"status.running\"),\n        success: t(\"common.success\"),\n        fail: t(\"common.failed\"),\n    };\n    return textMap[props.status as string] || \"Unknown\";\n});\n</script>\n\n<template>\n    <div\n        class=\"text-white px-2 py-1 rounded-full text-sm inline-flex items-center\"\n        :class=\"statusColor\"\n    >\n        <div class=\"w-2 h-2 rounded-full bg-white mr-2\"></div>\n        <a-tooltip\n            v-if=\"!!statusMsg && statusMsg.length < 20\"\n            :content=\"statusMsg\"\n            position=\"left\"\n            mini\n        >\n            <div>\n                {{ statusText }}\n                <icon-info-circle />\n            </div>\n        </a-tooltip>\n        <a-popover v-else-if=\"!!statusMsg\" position=\"left\">\n            <div>\n                {{ statusText }}\n                <icon-info-circle />\n            </div>\n            <template #content>\n                <div class=\"w-96 h-32 overflow-auto -m-3\">\n                    <pre class=\"text-xs\">{{ statusMsg }}</pre>\n                </div>\n            </template>\n        </a-popover>\n        <div v-else>{{ statusText }}</div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/components/common/UpdaterButton.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport { AppConfig } from \"../../config\";\nimport { doCheckForUpdate } from \"./util\";\n\nconst updaterCheckLoading = ref(false);\nconst checkAtLaunch = ref<\"yes\" | \"no\">(\"no\");\n\nonMounted(() => {\n    loadCheckAtLaunch();\n});\n\nconst loadCheckAtLaunch = async () => {\n    checkAtLaunch.value = await window.$mapi.updater.getCheckAtLaunch();\n};\n\nconst onCheckAtLaunchChange = async (value: boolean) => {\n    await window.$mapi.updater.setCheckAtLaunch(value ? \"yes\" : \"no\");\n    await loadCheckAtLaunch();\n};\n\nconst doVersionCheck = async () => {\n    updaterCheckLoading.value = true;\n    await doCheckForUpdate(true);\n    updaterCheckLoading.value = false;\n};\n</script>\n\n<template>\n    <div class=\"inline-flex items-center\">\n        <a-button\n            v-if=\"!!AppConfig.updaterUrl\"\n            size=\"mini\"\n            class=\"mr-2\"\n            :loading=\"updaterCheckLoading\"\n            @click=\"doVersionCheck()\"\n        >\n            {{ $t(\"update.check\") }}\n        </a-button>\n        <a-checkbox\n            :model-value=\"checkAtLaunch === 'yes'\"\n            @change=\"onCheckAtLaunchChange as any\"\n        >\n            {{ $t(\"setting.autoUpdate\") }}\n        </a-checkbox>\n    </div>\n</template>\n"
  },
  {
    "path": "src/components/common/VideoPlayer.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from \"vue\";\nimport Player from \"xgplayer\";\nimport \"xgplayer/dist/index.min.css\";\n\nconst videoContainer = ref<HTMLDivElement | undefined>(undefined);\n\nconst props = withDefaults(\n    defineProps<{\n        url?: string;\n        width?: string;\n        height?: string;\n        autoplay?: boolean;\n        autoplayMuted?: boolean;\n        loop?: boolean;\n        controls?: boolean;\n    }>(),\n    {\n        url: \"\",\n        width: \"100%\",\n        height: \"100%\",\n        autoplay: false,\n        autoplayMuted: false,\n        loop: false,\n        controls: true,\n    },\n);\n\nlet player: Player | null = null;\n\nconst initPlayer = () => {\n    if (player) {\n        player.destroy();\n        player = null;\n    }\n    if (videoContainer.value && props.url) {\n        let url = props.url;\n        if (\n            url.startsWith(\"http:\") ||\n            url.startsWith(\"https:\") ||\n            url.startsWith(\"file:\")\n        ) {\n            // do nothing\n        } else {\n            url = \"file://\" + url;\n        }\n        player = new Player({\n            el: videoContainer.value,\n            url: url,\n            width: props.width,\n            height: props.height,\n            autoplay: props.autoplay,\n            muted: props.autoplayMuted,\n            loop: props.loop,\n            controls: props.controls,\n            volume: 1.0,\n        });\n    }\n};\n\nwatch(\n    () => props.url,\n    (newUrl) => {\n        initPlayer();\n    },\n);\n\nonMounted(() => {\n    initPlayer();\n});\n\nonBeforeUnmount(() => {\n    if (player) {\n        player.destroy();\n    }\n});\n</script>\n\n<template>\n    <div\n        ref=\"videoContainer\"\n        :style=\"{ width: props.width, height: props.height }\"\n    ></div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/WebFileSelectButton.vue",
    "content": "<script setup lang=\"ts\">\nconst props = defineProps({\n    accept: {\n        type: String,\n        default: \"\",\n    },\n});\nconst emit = defineEmits({\n    selectFile: (file: File) => true,\n});\n\nconst onWebSelectFile = async (fileList, fileItem) => {\n    const file = fileItem.file;\n    emit(\"selectFile\", file);\n    fileList.value = [];\n};\n</script>\n\n<template>\n    <a-upload\n        :auto-upload=\"false\"\n        :show-file-list=\"false\"\n        :accept=\"accept\"\n        @change=\"onWebSelectFile\"\n    >\n        <template #upload-button>\n            <slot></slot>\n        </template>\n    </a-upload>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/common/dataConfig.ts",
    "content": "export const getDataContent = async <T extends any>(\n    key: string,\n    defaultValue: T,\n): Promise<T> => {\n    return $mapi.storage.get(\"data\", key, defaultValue);\n};\n"
  },
  {
    "path": "src/components/common/index.ts",
    "content": ""
  },
  {
    "path": "src/components/common/util.ts",
    "content": "import { onMounted, toRaw, watch } from \"vue\";\nimport { AppConfig } from \"../../config\";\nimport { t } from \"../../lang\";\nimport { defaultResponseProcessor } from \"../../lib/api\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { StorageUtil } from \"../../lib/storage\";\nimport { VersionUtil } from \"../../lib/util\";\n\nexport const doCopy = async (\n    text: string | object,\n    successTip: string = \"\",\n): Promise<void> => {\n    successTip = successTip || t(\"common.copySuccess\");\n    text = typeof text === \"object\" ? JSON.stringify(text) : String(text);\n    await window.$mapi.app.setClipboardText(text);\n    Dialog.tipSuccess(successTip);\n};\n\nexport const doSaveFile = async (filePath: string) => {\n    try {\n        const options: any = {\n            defaultPath: window.$mapi.file.pathToName(filePath, true, -1),\n        };\n        const savePath = await window.$mapi.file.openSave(options);\n        if (savePath) {\n            await window.$mapi.file.copy(filePath, savePath, {\n                isDataPath: false,\n            });\n            Dialog.tipSuccess(t(\"msg.fileSavedTo\", { path: savePath }));\n        }\n    } catch (error) {\n        Dialog.tipError(\n            t(\"error.saveFileFailed\", {\n                error: (error as Error).message || error,\n            }),\n        );\n    }\n};\n\nexport const doOpenFile = async (options?: {\n    extensions?: string[];\n    multiple?: boolean;\n}): Promise<string | string[] | undefined> => {\n    options = Object.assign(\n        {\n            extensions: [],\n            multiple: false,\n        },\n        options,\n    );\n    try {\n        const opt: any = {};\n        if (options.extensions && options.extensions.length > 0) {\n            opt.filters = [\n                {\n                    extensions: toRaw(options.extensions),\n                },\n            ];\n        }\n        if (options.multiple) {\n            opt.properties = [\"multiSelections\"];\n        }\n        const result = await window.$mapi.file.openFile(opt);\n        if (result) {\n            return result;\n        }\n    } catch (error) {\n        Dialog.tipError(t(\"error.selectFileFailed\", { error }));\n    }\n};\n\nexport const doOpenBrowserFile = (options: {\n    accept: string;\n    multiple: boolean;\n    max?: string;\n}): Promise<File | null> => {\n    options = Object.assign({\n        accept: \"*/*\",\n        multiple: false,\n        max: undefined,\n    });\n    const compareSize = (size: number, target: string): boolean => {\n        const k = 1024;\n        const sizes = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"];\n        const i = sizes.findIndex((item) => item === target.replace(/\\d+/, \"\"));\n        return size > parseInt(target) * k ** i;\n    };\n    return new Promise((resolve, reject) => {\n        // 创建input[file]元素\n        const input = document.createElement(\"input\");\n        // 设置相应属性\n        input.setAttribute(\"type\", \"file\");\n        input.setAttribute(\"accept\", options.accept);\n        if (options.multiple) {\n            input.setAttribute(\"multiple\", \"multiple\");\n        } else {\n            input.removeAttribute(\"multiple\");\n        }\n        // 绑定事件\n        input.onchange = function () {\n            // @ts-ignore\n            let files: File[] = Array.from(this.files);\n            if (files) {\n                const length = files.length;\n                files = files.filter((file) => {\n                    if (options.max) {\n                        return !compareSize(file.size, options.max);\n                    } else {\n                        return true;\n                    }\n                });\n                if (files && files.length > 0) {\n                    if (length !== files.length) {\n                        // message.warning(`已过滤上传文件中大小大于${options.max}的文件`);\n                    }\n                    resolve(files[0]);\n                } else {\n                    Dialog.tipError(\n                        t(\"error.fileSizeExceedMax\", { max: options.max }),\n                    );\n                    resolve(null);\n                }\n            } else {\n                reject(null);\n            }\n        };\n        input.oncancel = function () {\n            reject(null);\n        };\n        input.click();\n    });\n};\n\nexport const doCheckForUpdate = async (noticeLatest?: boolean) => {\n    const res = await window.$mapi.updater.checkForUpdate();\n    defaultResponseProcessor(res, (res: ApiResult<any>) => {\n        if (!res.data.version) {\n            Dialog.tipError(t(\"update.checkFailed\"));\n            return;\n        }\n        if (VersionUtil.le(res.data.version, AppConfig.version)) {\n            if (noticeLatest) {\n                Dialog.tipSuccess(t(\"update.alreadyLatest\"));\n            }\n            return;\n        }\n        Dialog.confirm(\n            t(\"update.newVersionFound\", { version: res.data.version }),\n        ).then(() => {\n            window.$mapi.app.openExternal(AppConfig.downloadUrl);\n        });\n    });\n};\n\nexport const dataAutoSaveDraft = (\n    key: string,\n    data: any,\n    option?: {\n        type: \"object\" | \"array\";\n        confirmText?: string | null;\n    },\n) => {\n    option = Object.assign(\n        {\n            type: \"object\",\n            confirmText: null,\n        },\n        option,\n    );\n    const load = async () => {\n        const value = await result();\n        if (\"object\" === option?.type) {\n            if (value) {\n                if (option.confirmText) {\n                    await Dialog.confirm(option.confirmText);\n                }\n                for (const k in value) {\n                    if (Object.prototype.hasOwnProperty.call(value, k)) {\n                        data[k] = value[k];\n                    }\n                }\n            }\n        } else if (\"array\" === option?.type) {\n            if (Array.isArray(value) && value.length > 0) {\n                if (option.confirmText) {\n                    await Dialog.confirm(option.confirmText);\n                }\n                data.splice(0, data.length, ...value);\n            }\n        }\n    };\n    onMounted(async () => [await load()]);\n    watch(\n        () => data,\n        async (value) => {\n            // console.log('data changed, save draft to local storage', key, value);\n            StorageUtil.set(key, value);\n        },\n        {\n            deep: true,\n        },\n    );\n    const clearDraft = () => {\n        StorageUtil.remove(key);\n    };\n    const result = async () => {\n        if (\"object\" === option?.type) {\n            return StorageUtil.getObject(key);\n        } else if (\"array\" === option?.type) {\n            return StorageUtil.getArray(key);\n        }\n        throw new Error(\"dataAutoSaveDraft: unknown type\" + option?.type);\n    };\n    return {\n        clearDraft,\n        load,\n    };\n};\n"
  },
  {
    "path": "src/config.ts",
    "content": "import packageJson from \"../package.json\";\n\nconst BASE_URL = \"https://focusany.com\";\n// const BASE_URL = 'http://focusany.demo.soft.host';\n\nexport const AppConfig = {\n    name: \"FocusAny\",\n    title: \"FocusAny\",\n    slogan: \"专注提效的AI工具条\",\n    version: packageJson.version,\n    website: `${BASE_URL}`,\n    websiteGithub: \"https://github.com/modstart-lib/focusany\",\n    websiteGitee: \"https://gitee.com/modstart-lib/focusany\",\n    apiBaseUrl: `${BASE_URL}/api`,\n    updaterUrl: `${BASE_URL}/app_manager/updater/open`,\n    downloadUrl: `${BASE_URL}/app_manager/download`,\n    feedbackUrl: `${BASE_URL}/feedback_ticket`,\n    statisticsUrl: `${BASE_URL}/app_manager/collect`,\n    guideUrl: `${BASE_URL}/app_manager/guide`,\n    helpUrl: `${BASE_URL}/app_manager/help`,\n    basic: {\n        userEnable: false,\n    },\n};\n"
  },
  {
    "path": "src/declarations/svg.d.ts",
    "content": "declare module \"*.svg\" {\n    const content: string;\n    export default content;\n}\n"
  },
  {
    "path": "src/declarations/type.d.ts",
    "content": "type DefsPage = {\n    onShow: (cb: Function) => void;\n    onHide: (cb: Function) => void;\n    onMaximize: (cb: Function) => void;\n    onUnmaximize: (cb: Function) => void;\n    onEnterFullScreen: (cb: Function) => void;\n    onLeaveFullScreen: (cb: Function) => void;\n    onBroadcast: (type: string, cb: (data: any) => void) => void;\n    offBroadcast: (type: string, cb: (data: any) => void) => void;\n    registerCallPage: (\n        name: string,\n        cb: (\n            resolve: (data: any) => void,\n            reject: (error: string) => void,\n            data: any,\n        ) => void,\n    ) => void;\n    createChannel: (cb: (data: any) => void) => string;\n    destroyChannel: (channel: string) => void;\n    ipcSendToHost: (channel: string, type: string, data?: any) => void;\n    ipcSend: (channel: string, type: string, data?: any) => void;\n\n    onPluginInit: (cb: Function) => void;\n    onPluginInitReady: (cb: Function) => void;\n    onPluginAlreadyOpened: (cb: Function) => void;\n    onPluginExit: (cb: Function) => void;\n    onPluginDetached: (cb: Function) => void;\n    onPluginState: (cb: Function) => void;\n    onPluginCodeInit: (cb: Function) => void;\n    onPluginCodeSetting: (cb: Function) => void;\n    onPluginCodeData: (cb: Function) => void;\n    onPluginCodeExit: (cb: Function) => void;\n    onSetSubInput: (cb: Function) => void;\n    onRemoveSubInput: (cb: Function) => void;\n    onSetSubInputValue: (cb: Function) => void;\n    onDetachSet: (cb: Function) => void;\n    onDetachWindowClosed: (cb: Function) => void;\n};\ntype DefsMapi = {\n    app: {\n        getPreload: () => Promise<string>;\n        resourcePathResolve: (filePath: string) => Promise<string>;\n        extraPathResolve: (filePath: string) => Promise<string>;\n        platformName: () => \"win\" | \"osx\" | \"linux\" | null;\n        platformArch: () => \"x86\" | \"arm64\" | null;\n        isPlatform: (platform: \"win\" | \"osx\" | \"linux\") => boolean;\n        quit: () => Promise<void>;\n        restart: () => Promise<void>;\n        windowMin: (name?: string) => Promise<void>;\n        windowMax: (name?: string) => Promise<void>;\n        windowSetSize: (\n            name: string | null,\n            width: number,\n            height: number,\n            option?: {\n                includeMinimumSize?: boolean;\n                center?: boolean;\n            },\n        ) => Promise<void>;\n        windowOpen: (name: string, option?: any) => Promise<void>;\n        windowHide: (name?: string) => Promise<void>;\n        windowClose: (name?: string) => Promise<void>;\n        windowMove: (\n            name: string | null,\n            data: {\n                mouseX: number;\n                mouseY: number;\n                width: number;\n                height: number;\n            },\n        ) => Promise<void>;\n        openExternal: (url: string) => Promise<void>;\n        openPath: (url: string) => Promise<void>;\n        showItemInFolder: (url: string) => Promise<void>;\n        appEnv: () => Promise<any>;\n        setRenderAppEnv: (env: any) => Promise<void>;\n        isDarkMode: () => Promise<boolean>;\n        shell: (\n            command: string,\n            option?: {\n                cwd?: string;\n                outputEncoding?: string;\n                shell?: boolean;\n            },\n        ) => Promise<void>;\n        spawnShell: (\n            command: string | string[],\n            option: {\n                stdout?: (data: string, process: any) => void;\n                stderr?: (data: string, process: any) => void;\n                success?: (process: any) => void;\n                error?: (msg: string, exitCode: number, process: any) => void;\n                cwd?: string;\n                outputEncoding?: string;\n                env?: Record<string, any>;\n                shell?: boolean;\n            } | null,\n        ) => Promise<{\n            stop: () => void;\n            send: (data: any) => void;\n            result: () => Promise<string>;\n        }>;\n        spawnBinary: (\n            binary: string,\n            args: string[],\n            option?: {\n                stdout?: (data: string, process: any) => void;\n                stderr?: (data: string, process: any) => void;\n                success?: (process: any) => void;\n                error?: (msg: string, exitCode: number, process: any) => void;\n                cwd?: string;\n                outputEncoding?: string;\n                env?: Record<string, any>;\n                shell?: boolean;\n            } | null,\n        ) => Promise<string>;\n        availablePort: (\n            start: number,\n            lockKey?: string,\n            lockTime?: number,\n        ) => Promise<number>;\n        fixExecutable: (executable: string) => Promise<void>;\n        getClipboardText: () => Promise<string>;\n        setClipboardText: (text: string) => Promise<void>;\n        getClipboardImage: () => Promise<string>;\n        setClipboardImage: (image: string) => Promise<void>;\n        getUserAgent: () => string;\n        toast: (\n            msg: string,\n            option?: {\n                duration?: number;\n                status?: \"success\" | \"error\";\n            },\n        ) => Promise<void>;\n        setupList: () => Promise<\n            {\n                name: string;\n                title: string;\n                status: \"success\" | \"fail\";\n                desc: string;\n                steps: {\n                    title: string;\n                    image: string;\n                }[];\n            }[]\n        >;\n        setupOpen: (name: string) => Promise<void>;\n        setupIsOk: () => Promise<boolean>;\n        getBuildInfo: () => Promise<{\n            buildTime: string;\n        }>;\n        collect: (options?: {}) => Promise<any>;\n        setAutoLaunch: (enable: boolean, options?: {}) => Promise<boolean>;\n        getAutoLaunch: (options?: {}) => Promise<boolean>;\n    };\n    config: {\n        get: (key: string, defaultValue: any = null) => Promise<any>;\n        set: (key: string, value: any) => Promise<void>;\n        all: () => Promise<any>;\n        getEnv: (key: string, defaultValue: any = null) => Promise<any>;\n        setEnv: (key: string, value: any) => Promise<void>;\n        allEnv: () => Promise<any>;\n    };\n    log: {\n        root: () => string;\n        info: (msg: string, data: any = null) => Promise<void>;\n        error: (msg: string, data: any = null) => Promise<void>;\n        collect: (option?: {\n            startTime?: string;\n            endTime?: string;\n            limit?: number;\n        }) => Promise<string>;\n    };\n    storage: {\n        all: () => Promise<any>;\n        get: (group: string, key: string, defaultValue: any) => Promise<any>;\n        set: (group: string, key: string, value: any) => Promise<void>;\n        write: (group: string, value: any) => Promise<void>;\n        read: (group: string, defaultValue: any = null) => Promise<any>;\n    };\n    db: {\n        execute: (sql: string, params: any = []) => Promise<any>;\n        insert: (sql: string, params: any = []) => Promise<any>;\n        first: (sql: string, params: any = []) => Promise<any>;\n        select: (sql: string, params: any = []) => Promise<any>;\n        update: (sql: string, params: any = []) => Promise<any>;\n        delete: (sql: string, params: any = []) => Promise<any>;\n    };\n    kvdb: {\n        put: (name: string, data: Doc<any>) => Promise<any>;\n        putForce: (name: string, data: Doc<any>) => Promise<any>;\n        get: (name: string, id: string) => Promise<any>;\n        remove: (name: string, doc: Doc<any> | string) => Promise<any>;\n        bulkDocs: (name: string, docs: any[]) => Promise<any>;\n        allDocs: (name: string, key: string) => Promise<any>;\n        allKeys: (name: string, key: string) => Promise<string[]>;\n        count: (name: string, key: string) => Promise<any>;\n        postAttachment: (\n            name: string,\n            docId: string,\n            attachment: any,\n            type: string,\n        ) => Promise<any>;\n        getAttachment: (name: string, docId: string) => Promise<any>;\n        getAttachmentType: (name: string, docId: string) => Promise<any>;\n        dumpToFile: (file: string) => Promise<void>;\n        importFromFile: (file: string) => Promise<void>;\n        testWebdav: (option: {\n            url: string;\n            username: string;\n            password: string;\n        }) => Promise<void>;\n        dumpToWebDav: (\n            file: string,\n            option: {\n                url: string;\n                username: string;\n                password: string;\n            },\n        ) => Promise<void>;\n        importFromWebDav: (\n            file: string,\n            option: {\n                url: string;\n                username: string;\n                password: string;\n            },\n        ) => Promise<void>;\n        listWebDav: (\n            dir: string,\n            option: {\n                url: string;\n                username: string;\n                password: string;\n            },\n        ) => Promise<any[]>;\n    };\n    file: {\n        fullPath: (path: string) => Promise<string>;\n        exists: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<boolean>;\n        isDirectory: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<boolean>;\n        mkdir: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        list: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<\n            {\n                name: string;\n                pathname: string;\n                isDirectory: boolean;\n                size: number;\n                lastModified: number;\n            }[]\n        >;\n        listAll: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<any[]>;\n        write: (\n            path: string,\n            data: any,\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        writeBuffer: (\n            path: string,\n            data: any,\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        writeStream: (\n            path: string,\n            stream: any,\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        read: (path: string, option?: { isDataPath?: boolean }) => Promise<any>;\n        readBuffer: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<any>;\n        readStream: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<ReadableStream | null>;\n        deletes: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        clean: (\n            paths: string[],\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        rename: (\n            pathOld: string,\n            pathNew: string,\n            option?: {\n                isDataPath?: boolean;\n                overwrite?: boolean;\n            },\n        ) => Promise<void>;\n        copy: (\n            pathOld: string,\n            pathNew: string,\n            option?: {\n                isDataPath?: boolean;\n                overwrite?: boolean;\n            },\n        ) => Promise<void>;\n        temp: (\n            ext: string = \"tmp\",\n            prefix: string = \"file\",\n            suffix: string = \"\",\n        ) => Promise<string>;\n        tempDir: (prefix: string = \"dir\") => Promise<string>;\n        watchText: (\n            path: string,\n            callback: (data: {}) => void,\n            option?: {\n                isDataPath?: boolean;\n                limit?: number;\n            },\n        ) => Promise<{\n            stop: Function;\n        }>;\n        appendText: (\n            path: string,\n            data: any,\n            option?: { isDataPath?: boolean },\n        ) => Promise<void>;\n        download: (\n            url: string,\n            path?: string | null,\n            option?: {\n                isDataPath?: boolean;\n                userAgent?: string;\n                progress?: (percent: number, total: number) => void;\n            },\n        ) => Promise<string>;\n        openFile: (\n            options: {\n                filters?: {\n                    name: string;\n                    extensions: string[];\n                }[];\n                properties?: \"multiSelections\"[];\n            } = {},\n        ) => Promise<string | null>;\n        openDirectory: (options: {} = {}) => Promise<string | null>;\n        openSave: (options: {} = {}) => Promise<string | null>;\n        ext: (path: string) => Promise<string>;\n        stat: (\n            path: string,\n            option?: { isDataPath?: boolean },\n        ) => Promise<{\n            size: number;\n            isDirectory: boolean;\n            lastModified: number;\n        }>;\n        textToName: (\n            text: string,\n            ext: string = \"\",\n            maxLimit: number = 100,\n        ) => string;\n        pathToName: (\n            path: string,\n            includeExt: boolean = true,\n            maxLimit: number = 100,\n        ) => string;\n        hubRootDefault: () => Promise<string>;\n        hubRoot: () => Promise<string>;\n        hubSave: (\n            file: string,\n            option?: {\n                ext?: string;\n                returnFullPath?: boolean;\n                ignoreWhenInHub?: boolean;\n                cleanOld?: boolean;\n                saveGroup?: string;\n                savePath?: string;\n                savePathParam?: {\n                    [key: string]: any;\n                };\n            },\n        ) => Promise<string>;\n        hubSaveContent: (\n            content: string,\n            option: {\n                ext: string;\n                returnFullPath?: boolean;\n                saveGroup?: string;\n                savePath?: string;\n                savePathParam?: {\n                    [key: string]: any;\n                };\n            },\n        ) => Promise<string>;\n        hubDelete: (\n            file: string,\n            option?: {\n                isDataPath?: boolean;\n                ignoreWhenNotInHub?: boolean;\n                tryLaterWhenFailed?: boolean;\n            },\n        ) => Promise<void>;\n        hubFullPath: (file: string) => Promise<string>;\n        hubFile: (\n            ext: string,\n            option?: {\n                returnFullPath?: boolean;\n                saveGroup?: string;\n                savePath?: string;\n                savePathParam?: {\n                    [key: string]: any;\n                };\n            },\n        ) => Promise<string>;\n        isHubFile: (file: string) => Promise<boolean>;\n        cacheForget: (key: any) => Promise<void>;\n        cacheSet: (key: any, data: any) => Promise<void>;\n        cacheGet: <T>(key: any) => Promise<T | null>;\n        cacheGetPath: (key: any) => Promise<string | null>;\n    };\n    updater: {\n        checkForUpdate: () => Promise<ApiResult<any>>;\n        getCheckAtLaunch: () => Promise<\"yes\" | \"no\">;\n        setCheckAtLaunch: (value: \"yes\" | \"no\") => Promise<void>;\n    };\n    statistics: {\n        tick: (name: string, data: any = null) => Promise<void>;\n    };\n    event: {\n        send: (name: string, type: string, data: any) => void;\n        callPage: (\n            name: string,\n            type: string,\n            data?: any,\n            option?: {\n                waitReadyTimeout?: number;\n                timeout?: number;\n            },\n        ) => Promise<ApiResult<any>>;\n        channelSend: (channel: string, data: any) => Promise<void>;\n    };\n    user: {\n        open: (option?: {\n            readyParam: {\n                page?: string;\n                [key: string]: any;\n            };\n        }) => Promise<void>;\n        get: () => Promise<{\n            apiToken: string;\n            user: {\n                id: string;\n                name: string;\n                avatar: string;\n            };\n            data: {};\n            basic: {};\n        }>;\n        refresh: () => Promise<void>;\n        getApiToken: () => Promise<string>;\n        getWebEnterUrl: (url: string) => Promise<string>;\n        openWebUrl: (url: string) => Promise<void>;\n        apiPost: (\n            url: string,\n            data: Record<string, any>,\n            option?: {\n                throwException?: boolean;\n            },\n        ) => Promise<any>;\n    };\n    misc: {\n        getZipFileContent: (path: string, pathInZip: string) => Promise<string>;\n        unzip: (\n            zipPath: string,\n            dest: string,\n            option?: { process: Function },\n        ) => Promise<void>;\n        zip: (\n            zipPath: string,\n            sourceDir: string,\n            option?: { end?: (archive: any) => void },\n        ) => Promise<void>;\n        request: (option: {\n            url: string;\n            method?: \"GET\" | \"POST\";\n            responseType?: \"json\" | \"text\" | \"arraybuffer\";\n            headers?: any;\n            data?: any;\n        }) => Promise<any>;\n    };\n    manager: {\n        getConfig: () => Promise<ConfigRecord>;\n        setConfig: (config: ConfigRecord) => Promise<void>;\n\n        getMcpServer: () => Promise<string>;\n        getMcpInfo: () => Promise<{\n            tools: { name: string; description: string }[];\n        }>;\n\n        isShown: () => Promise<boolean>;\n        show: () => Promise<void>;\n        hide: () => Promise<void>;\n\n        getClipboardContent: () => Promise<ClipboardDataType | null>;\n        getClipboardChangeTime: () => Promise<number>;\n        getSelectedContent: () => Promise<SelectedContent>;\n\n        searchFastPanelAction: (\n            query: {\n                currentFiles: any[];\n                currentImage: string;\n            },\n            option?: {},\n        ) => Promise<{\n            matchActions: ActionRecord[];\n            viewActions: ActionRecord[];\n        }>;\n        listDetachWindowActions: (option?: {}) => Promise<ActionRecord[]>;\n        searchAction: (\n            query: {\n                keywords: string;\n                currentFiles: any[];\n                currentImage: string;\n            },\n            option?: {},\n        ) => Promise<{\n            detachWindowActions: ActionRecord[];\n            searchActions: ActionRecord[];\n            matchActions: ActionRecord[];\n            viewActions: ActionRecord[];\n            historyActions: ActionRecord[];\n            pinActions: ActionRecord[];\n        }>;\n        subInputChange: (keywords: string, option?: {}) => void;\n\n        openPlugin: (pluginName: string, option?: {}) => Promise<void>;\n        openAction: (action: ActionRecord) => Promise<void>;\n        openActionCode: (id: string | null) => Promise<void>;\n        searchActionCode: (keywords: string | null) => Promise<void>;\n        openActionWindow: (type: \"open\", action: ActionRecord) => Promise<void>;\n\n        closeMainPlugin: (option?: {}) => Promise<void>;\n        openMainPluginDevTools: (option?: {}) => Promise<void>;\n        openMainPluginLog: (option?: {}) => Promise<void>;\n\n        detachPlugin: (option?: {}) => Promise<void>;\n        listPlugin: (option?: {}) => Promise<PluginRecord[]>;\n        installPlugin: (fileOrPath: string, option?: {}) => Promise<void>;\n        refreshInstallPlugin: (\n            pluginName: string,\n            option?: {},\n        ) => Promise<void>;\n        uninstallPlugin: (pluginName: string, option?: {}) => Promise<void>;\n        getPluginInstalledVersion: (\n            pluginName: string,\n            option?: {},\n        ) => Promise<boolean>;\n        listDisabledActionMatch: (option?: {}) => Promise<any>;\n        toggleDisabledActionMatch: (\n            pluginName: string,\n            actionName: string,\n            matchName: string,\n            option?: {},\n        ) => Promise<boolean>;\n        listPinAction: (option?: {}) => Promise<any>;\n        togglePinAction: (\n            pluginName: string,\n            actionName: string,\n            option?: {},\n        ) => Promise<boolean>;\n        showLog: (pluginName: string, option?: {}) => Promise<void>;\n        clearCache: (option?: {}) => Promise<void>;\n        hotKeyWatch: (option?: {}) => Promise<void>;\n        hotKeyUnwatch: (option?: {}) => Promise<void>;\n\n        toggleDetachPluginAlwaysOnTop: (\n            alwaysOnTop: boolean,\n            option?: {},\n        ) => Promise<boolean>;\n        setDetachPluginZoom: (zoom: number, option?: {}) => Promise<void>;\n        firePluginMoreMenuClick: (name: string, option?: {}) => Promise<void>;\n        fireDetachOperateClick: (name: string, option?: {}) => Promise<void>;\n        closeDetachPlugin: (option?: {}) => Promise<void>;\n        openDetachPluginDevTools: (option?: {}) => Promise<void>;\n        openDetachPluginLog: (option?: {}) => Promise<void>;\n        setPluginAutoDetach: (\n            autoDetach: boolean,\n            option?: {},\n        ) => Promise<void>;\n        getPluginConfig: (\n            pluginName: string,\n            option?: {},\n        ) => Promise<PluginConfig>;\n\n        listFilePluginRecords: (option?: {}) => Promise<FilePluginRecord[]>;\n        updateFilePluginRecords: (\n            records: FilePluginRecord[],\n            option?: {},\n        ) => Promise<void>;\n\n        listLaunchRecords: (option?: {}) => Promise<LaunchRecord[]>;\n        updateLaunchRecords: (\n            records: LaunchRecord[],\n            option?: {},\n        ) => Promise<void>;\n\n        storeInstall: (\n            pluginName: string,\n            option?: {\n                version?: string;\n            },\n        ) => Promise<void>;\n        storePublish: (\n            pluginName: string,\n            option?: {\n                version?: string;\n            },\n        ) => Promise<BaseResult>;\n        storePublishInfo: (\n            pluginName: string,\n            option?: {\n                version?: string;\n            },\n        ) => Promise<void>;\n        storeInstallingInfo: (pluginName: string) => Promise<{\n            isInstalling: boolean;\n            percent: number;\n        }>;\n\n        historyClear: (option?: {}) => Promise<void>;\n        historyDelete: (\n            pluginName: string,\n            actionName: string,\n            option?: {},\n        ) => Promise<void>;\n    };\n};\n\ndeclare global {\n    interface Window {\n        __page: DefsPage;\n        $mapi: DefsMapi;\n        focusany: FocusAnyApi;\n    }\n\n    const __page: DefsPage;\n    const $mapi: DefsMapi;\n    const focusany: FocusAnyApi;\n}\n\nexport {};\n"
  },
  {
    "path": "src/entry/Page.vue",
    "content": "<template>\n    <a-config-provider :locale=\"locale\" :global=\"true\">\n        <div class=\"window-container\">\n            <div\n                class=\"window-header flex h-10 items-center border-b border-solid border-gray-200\"\n            >\n                <div class=\"window-header-title flex-grow flex items-center\">\n                    <div class=\"pl-2 py-2\">\n                        <img src=\"/logo.svg\" class=\"w-4 t-4\" />\n                    </div>\n                    <div\n                        class=\"p-2 flex-grow truncate overflow-hidden text-ellipsis max-w-96\"\n                    >\n                        {{ pageTitle }}\n                    </div>\n                </div>\n                <div class=\"p-1 leading-4\">\n                    <div\n                        class=\"inline-block w-6 h-6 leading-6 cursor-pointer hover:text-red-500\"\n                        @click=\"doClose\"\n                    >\n                        <icon-close class=\"text-sm\" />\n                    </div>\n                </div>\n            </div>\n            <div class=\"window-body\">\n                <component :is=\"props.page\" @event=\"onEvent\" />\n            </div>\n        </div>\n    </a-config-provider>\n</template>\n\n<script setup lang=\"ts\">\nimport { Component, computed, ref } from \"vue\";\nimport { onLocaleChange } from \"../lang\";\n\nimport enUS from \"@arco-design/web-vue/es/locale/lang/en-us\";\nimport zhCN from \"@arco-design/web-vue/es/locale/lang/zh-cn\";\n\nconst locales = {\n    \"zh-CN\": zhCN,\n    \"en-US\": enUS,\n};\n\nconst props = defineProps<{\n    name: string;\n    title: string;\n    page: Component;\n}>();\n\nconst pageTitleCustom = ref<string>(\"\");\nconst pageTitle = computed(() => {\n    if (pageTitleCustom.value) {\n        return pageTitleCustom.value;\n    }\n    return props.title;\n});\n\nconst onEvent = (type: string, data: Record<string, unknown>) => {\n    // console.log('Page.onEvent', type, data)\n    if (type === \"SetTitle\") {\n        pageTitleCustom.value = data.title;\n    }\n};\n\nconst doClose = async () => {\n    await window.$mapi.app.windowClose(props.name);\n};\n\nconst locale = ref(zhCN);\nonLocaleChange((newLocale) => {\n    locale.value = locales[newLocale];\n});\n</script>\n"
  },
  {
    "path": "src/entry/about.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageAbout from \"../pages/PageAbout.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"about\",\n    title: t(\"about.title\"),\n    page: PageAbout,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/detachWindow.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageDetachWindow from \"../pages/PageDetachWindow.vue\";\n\nconst app = createApp(PageDetachWindow);\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/fastPanel.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageFastPanel from \"../pages/PageFastPanel.vue\";\n\nconst app = createApp(PageFastPanel);\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/feedback.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageFeedback from \"../pages/PageFeedback.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"feedback\",\n    title: t(\"nav.feedback\"),\n    page: PageFeedback,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/guide.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageGuide from \"../pages/PageGuide.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"guide\",\n    title: t(\"nav.guide\"),\n    page: PageGuide,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/log.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageLog from \"../pages/PageLog.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"log\",\n    title: t(\"nav.log\"),\n    page: PageLog,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/monitor.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageMonitor from \"../pages/PageMonitor.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"monitor\",\n    title: t(\"common.loading\"),\n    page: PageMonitor,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/payment.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PagePayment from \"../pages/PagePayment.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"payment\",\n    title: t(\"page.payment.title\"),\n    page: PagePayment,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/setup.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageSetup from \"../pages/PageSetup.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"setup\",\n    title: t(\"page.setup.title\"),\n    page: PageSetup,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/store.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageStore from \"../pages/PageStore.vue\";\n\nconst app = createApp(PageStore);\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/system.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageSystem from \"../pages/PageSystem.vue\";\n\nconst app = createApp(PageSystem);\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/user.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageUser from \"../pages/PageUser.vue\";\nimport Page from \"./Page.vue\";\n\nconst app = createApp(Page, {\n    name: \"user\",\n    title: t(\"nav.userCenter\"),\n    page: PageUser,\n});\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/entry/workflow.ts",
    "content": "import { createApp } from \"vue\";\nimport store from \"../store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\n\nimport { i18n, t } from \"../lang\";\n\nimport { Dialog } from \"../lib/dialog\";\nimport \"../style.less\";\n\nimport PageWorkflow from \"../pages/PageWorkflow.vue\";\n\nconst app = createApp(PageWorkflow);\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n});\n"
  },
  {
    "path": "src/hooks/user.ts",
    "content": "import { ref } from \"vue\";\nimport { useUserStore } from \"../store/modules/user\";\nimport { useSettingStore } from \"../store/modules/setting\";\n\nconst setting = useSettingStore();\n\nexport const useUserPage = ({ web, status }) => {\n    const webPreload = ref(\"\");\n    const webUrl = ref(\"\");\n    const webUserAgent = window.$mapi.app.getUserAgent();\n\n    const user = useUserStore();\n    const canGoBack = ref(false);\n\n    const whiteUrl = [\n        \"/app_manager/user\",\n        \"/member_vip\",\n        \"/login\",\n        \"/register\",\n        \"/logout\",\n    ];\n    const urlMap = {\n        \"/app_manager/user\": \"/member\",\n    };\n\n    const getUrl = () => {\n        const url = web.value.getURL();\n        return new URL(url).pathname;\n    };\n\n    const getCanGoBack = () => {\n        if (whiteUrl[0] === getUrl()) {\n            return false;\n        }\n        return true;\n    };\n    const doBack = async () => {\n        web.value.loadURL(await user.webUrl());\n    };\n\n    const onMount = async () => {\n        web.value.addEventListener(\"did-fail-load\", (event: any) => {\n            status.value?.setStatus(\"fail\");\n        });\n        web.value.addEventListener(\"did-finish-load\", (event: any) => {\n            if (setting.shouldDarkMode()) {\n                web.value.executeJavaScript(\n                    `document.body.setAttribute('data-theme', 'dark');`,\n                );\n            }\n        });\n        web.value.addEventListener(\"close\", (event: any) => {\n            if (web.value.isDevToolsOpened()) {\n                web.value.closeDevTools();\n            }\n        });\n        web.value.addEventListener(\"dom-ready\", (e) => {\n            // web.value.openDevTools();\n            window.$mapi.user.refresh();\n            canGoBack.value = getCanGoBack();\n            web.value.executeJavaScript(`\ndocument.addEventListener('click', (event) => {\n    const target = event.target;\n    if (target.tagName !== 'A') return;\n    let url = target.href\n    if(url.startsWith('javascript:')) return;\n    let urlPath = new URL(url).pathname;\n    const urlMap = ${JSON.stringify(urlMap)};\n    if(urlMap[urlPath]) {\n        urlPath = urlMap[urlPath];\n        const urlNew = new URL(url);\n        urlNew.pathname = urlPath;\n        url = urlNew.toString();\n    }\n    const whiteList = ${JSON.stringify(whiteUrl)};\n    if (whiteList.includes(urlPath)) return;\n    event.preventDefault();\n    window.$mapi.user.openWebUrl(url)\n});\n`);\n            status.value?.setStatus(\"success\");\n            if (window.__page) {\n                window.__page.registerCallPage(\n                    \"ready\",\n                    (resolve, reject, data) => {\n                        web.value.executeJavaScript(\n                            `var call = function(){\n                            if(!window.__appManagerUserReady){\n                                setTimeout(call,10);\n                                return;\n                            };\n                            window.__appManagerUserReady(${JSON.stringify(data)});\n                         };call();`,\n                        );\n                        resolve(undefined);\n                    },\n                );\n            }\n        });\n        status.value?.setStatus(\"loading\");\n        webPreload.value = await window.$mapi.app.getPreload();\n        webUrl.value = await user.webUrl();\n    };\n\n    return {\n        webPreload,\n        webUrl,\n        webUserAgent,\n        user,\n        canGoBack,\n        doBack,\n        onMount,\n    };\n};\n"
  },
  {
    "path": "src/lang/en-US.json",
    "content": "{\n  \"config.slogan\": \"Focused AI Productivity Bar\",\n  \"about.disclaimer\": \"Disclaimer\",\n  \"about.license\": \"This product is open source software, following the AGPL-3.0 license agreement.\",\n  \"about.software\": \"About Software\",\n  \"about.title\": \"About\",\n  \"action.attrMatch\": \"Attribute Match\",\n  \"action.builtin\": \"Builtin\",\n  \"action.fileExtensions\": \"File Extensions\",\n  \"action.fileType\": \"File Type\",\n  \"action.matchAction\": \"Match Action\",\n  \"action.matchEditorHint\": \"When file matches the following extensions and types\",\n  \"action.matchExactKeyHint\": \"Input exactly equals the following keyword\",\n  \"action.matchFileHint\": \"When matched using the following rules\",\n  \"action.matchImageHint\": \"When an image is matched\",\n  \"action.matchKeywordHint\": \"Input matches the following keywords, including full pinyin and initials\",\n  \"action.matchRegexHint\": \"Input matches the following regex\",\n  \"action.matchWindowHint\": \"When the active window matches the following conditions\",\n  \"action.maxCount\": \"Max Count\",\n  \"action.minCount\": \"Min Count\",\n  \"action.nameMatch\": \"Name Match\",\n  \"action.pinToSearch\": \"Pin to Search Bar\",\n  \"action.plugin\": \"Plugin\",\n  \"action.regex\": \"Regex\",\n  \"action.searchAction\": \"Search Action\",\n  \"action.suffix\": \"Suffix\",\n  \"action.titleMatch\": \"Title Match\",\n  \"action.unpinFromSearch\": \"Unpin from Search Bar\",\n  \"action.window\": \"Window\",\n  \"avatar.addVideo\": \"Add Video Avatar\",\n  \"avatar.audioToVideo\": \"Audio Driven Lip Sync Video\",\n  \"avatar.avatar\": \"Digital Human Avatar\",\n  \"avatar.canOpenCloseMouth\": \"Mouth Can Open/Close\",\n  \"avatar.config\": \"Digital Human Configuration\",\n  \"avatar.digitalHuman\": \"Digital Human\",\n  \"avatar.example\": \"Avatar Example\",\n  \"avatar.faceInterference\": \"Face Interference\",\n  \"avatar.live\": \"Digital Human Live Stream\",\n  \"avatar.manage\": \"Manage Multiple Avatars\",\n  \"avatar.model\": \"Digital Human Model\",\n  \"avatar.oneClickSynthesis\": \"One-Click Synthesis\",\n  \"avatar.saveToMine\": \"Save to My Avatars\",\n  \"avatar.selfie\": \"Frontal Selfie\",\n  \"avatar.smartLive\": \"Smart Live Stream\",\n  \"avatar.synthesis\": \"Digital Human Synthesis\",\n  \"avatar.video\": \"Video Avatar\",\n  \"avatar.videoReq\": \"Video Avatar Requirements\",\n  \"backup.backingUp\": \"Backing up...\",\n  \"backup.backupFailed\": \"Backup Failed\",\n  \"backup.backupSuccess\": \"Backup Success\",\n  \"backup.backupToFile\": \"Backup to File\",\n  \"backup.backupToLocal\": \"Backup to Local\",\n  \"backup.formatTip\": \"Backup uses the backup format, regularly backing up files can avoid data loss.\",\n  \"backup.restoreFromFile\": \"Restore from File\",\n  \"backup.restoreFromLocal\": \"Restore from Local\",\n  \"backup.restoreFailed\": \"Restore Failed\",\n  \"backup.restoreSuccess\": \"Restore Success\",\n  \"backup.restoring\": \"Restoring...\",\n  \"backup.title\": \"Backup/Restore\",\n  \"common.adapt\": \"Adapt\",\n  \"common.add\": \"Add\",\n  \"common.addFile\": \"Add File\",\n  \"common.addOne\": \"Add One\",\n  \"common.aiGenerated\": \"AI Generated\",\n  \"common.all\": \"All\",\n  \"common.and\": \"And\",\n  \"common.availableVars\": \"Available Variables\",\n  \"common.back\": \"Back\",\n  \"common.batchInput\": \"Batch Input\",\n  \"common.batchPaste\": \"Batch Paste\",\n  \"common.batchPasteHint\": \"Batch paste, one per line\",\n  \"common.cancel\": \"Cancel\",\n  \"common.check\": \"Check\",\n  \"common.clearConfirm\": \"Confirm Clear?\",\n  \"common.clearHistory\": \"Clear History\",\n  \"common.clickTextToCopy\": \"Click text to copy\",\n  \"common.clickToConfig\": \"Click to Configure\",\n  \"common.clickToCopy\": \"Click to Copy\",\n  \"common.close\": \"Close\",\n  \"common.collapse\": \"Collapse\",\n  \"common.confirm\": \"Confirm\",\n  \"common.copySuccess\": \"Copy Success\",\n  \"common.copyText\": \"Copy Text\",\n  \"common.default\": \"Default\",\n  \"common.disable\": \"Disable\",\n  \"common.enable\": \"Enable\",\n  \"common.open\": \"Open\",\n  \"common.delete\": \"Delete\",\n  \"common.deleteConfirm\": \"Confirm Delete?\",\n  \"common.deleteRecordsConfirm\": \"Delete {count} records?\",\n  \"common.description\": \"Description\",\n  \"common.detail\": \"Details\",\n  \"common.docs\": \"Docs\",\n  \"common.download\": \"Download\",\n  \"common.downloadFailed\": \"Download Failed\",\n  \"common.downloadSuccess\": \"Download Successful\",\n  \"common.downloadingWait\": \"Downloading, please wait...\",\n  \"common.duration\": \"Duration\",\n  \"common.edit\": \"Edit\",\n  \"common.error\": \"Error\",\n  \"common.exit\": \"Exit\",\n  \"common.exitConfirm\": \"Confirm Exit?\",\n  \"common.expand\": \"Expand\",\n  \"common.extensions\": \"{extensions}\",\n  \"common.failed\": \"Failed\",\n  \"common.feature\": \"Feature\",\n  \"common.file\": \"File\",\n  \"common.find\": \"Find\",\n  \"common.folder\": \"Folder\",\n  \"common.generate\": \"Generate\",\n  \"common.generateFailed\": \"Generation Failed\",\n  \"common.hideWindow\": \"Hide Window\",\n  \"common.image\": \"Image\",\n  \"common.info\": \"Info\",\n  \"common.inputContent\": \"Input Content\",\n  \"common.key\": \"Key\",\n  \"common.keywords\": \"Keywords\",\n  \"common.language\": \"Language\",\n  \"common.loading\": \"Loading\",\n  \"common.loadingDots\": \"Loading...\",\n  \"common.localFile\": \"Local File\",\n  \"common.loginRequired\": \"Login Required\",\n  \"common.merge\": \"Merge\",\n  \"common.mergeWhitespace\": \"Merge Whitespace\",\n  \"common.modify\": \"Modify\",\n  \"common.more\": \" More\",\n  \"common.moreDetails\": \"More Details\",\n  \"common.name\": \"Name\",\n  \"common.no\": \"No\",\n  \"common.none\": \"None\",\n  \"common.notLoggedIn\": \"Not Logged In\",\n  \"common.officialSite\": \"Official Site\",\n  \"common.onlineDocs\": \"Online Docs\",\n  \"common.openFile\": \"Open File\",\n  \"common.openPath\": \"Open Path\",\n  \"common.pause\": \"Pause\",\n  \"common.pro\": \"Pro\",\n  \"common.recharge\": \"Recharge\",\n  \"common.refresh\": \"Refresh\",\n  \"common.rememberChoice\": \"Remember my choice\",\n  \"common.replace\": \"Replace\",\n  \"common.reselect\": \"Reselect\",\n  \"common.resolution\": \"Resolution\",\n  \"common.restoreDefault\": \"Restore Default\",\n  \"common.resumeSuccess\": \"Resume Success\",\n  \"common.retryAttempt\": \"Retry Attempt\",\n  \"common.retryFailed\": \"Retry Failed\",\n  \"common.retrySuccess\": \"Retry Success\",\n  \"common.save\": \"Save\",\n  \"common.saveSuccess\": \"Save Success\",\n  \"common.select\": \"Select\",\n  \"common.selectAll\": \"Select All\",\n  \"common.selectFile\": \"Select File\",\n  \"common.selectLocalFile\": \"Select Local File\",\n  \"common.selectPath\": \"Select Path\",\n  \"common.sendFailed\": \"Send Failed\",\n  \"common.sendSuccess\": \"Send Success\",\n  \"common.service\": \"Service\",\n  \"common.setting\": \"Settings\",\n  \"common.settingSuccess\": \"Setting Success\",\n  \"common.split\": \"Split\",\n  \"common.stopFailed\": \"Stop Failed\",\n  \"common.stopService\": \"Stop Service\",\n  \"common.stopping\": \"Stopping\",\n  \"common.submitConfirm\": \"Confirm Submit\",\n  \"common.success\": \"Success\",\n  \"common.system\": \"System\",\n  \"common.tag\": \"Tag\",\n  \"common.test\": \"Test\",\n  \"common.testFailed\": \"Test Failed\",\n  \"common.testSuccess\": \"Test Success\",\n  \"common.testing\": \"Testing, please wait...\",\n  \"common.tip\": \"Tip\",\n  \"common.totalCount\": \"Total {count} items\",\n  \"common.totalWords\": \"Total {count} words\",\n  \"common.type\": \"Type\",\n  \"common.useNow\": \"Use Now\",\n  \"common.value\": \"Value\",\n  \"common.version\": \"Version\",\n  \"common.view\": \"View\",\n  \"common.viewCode\": \"View Code\",\n  \"common.viewEffect\": \"View Effect\",\n  \"common.viewRecord\": \"View Record\",\n  \"common.vipRequired\": \"VIP Required\",\n  \"common.yes\": \"Yes\",\n  \"dashboard.statistics\": \"Statistics\",\n  \"dashboard.today\": \"Today\",\n  \"dashboard.todayTotalTasks\": \"Today's Total Tasks\",\n  \"desc.img2img\": \"Generate new image based on input image + description prompt\",\n  \"desc.longTextToAudio\": \"Convert long text content to audio file\",\n  \"desc.recognitionDownload\": \"Recognize file and download text/subtitle\",\n  \"desc.recognitionEdit\": \"Recognize audio file, support editing/downloading text/subtitle file after recognition\",\n  \"desc.subtitleToAudio\": \"Convert subtitle file to audio file\",\n  \"desc.txt2img\": \"Generate image based on text description\",\n  \"desc.videoVoiceReplace\": \"Replace human voice in video with other timbre\",\n  \"download.audio\": \"Download Audio\",\n  \"download.subtitleFile\": \"Download Subtitle File\",\n  \"download.textFile\": \"Download Text File\",\n  \"empty.noDownloadRecord\": \"No download records available\",\n  \"empty.noEditableData\": \"No editable data\",\n  \"empty.noLocalModel\": \"No local model available\",\n  \"empty.noLog\": \"No logs\",\n  \"empty.noLogFile\": \"No log file\",\n  \"empty.noModel\": \"No model available\",\n  \"empty.noModelAdd\": \"No models yet, please add a model~\",\n  \"empty.noModelPlatform\": \"No relevant model platform found\",\n  \"empty.noRecognitionTask\": \"No voice recognition tasks\",\n  \"empty.noRecord\": \"No records available\",\n  \"empty.noVoiceTask\": \"No voice synthesis tasks\",\n  \"error.pluginAlreadyExists\": \"Plugin Already Exists\",\n  \"error.pluginEditionNotMatch\": \"FocusAny Edition Does Not Meet Plugin Requirements\",\n  \"error.pluginFormatError\": \"Plugin Format Error\",\n  \"error.pluginNotExists\": \"Plugin Does Not Exist\",\n  \"error.pluginNotSupportPlatform\": \"Plugin Does Not Support Current Platform\",\n  \"error.pluginReleaseDocFormatError\": \"Plugin Release Document Format Error\",\n  \"error.pluginReleaseDocNotFound\": \"Plugin Release Document Not Found\",\n  \"error.pluginVersionNotMatch\": \"FocusAny Version Does Not Meet Plugin Requirements\",\n  \"error.publishVersionNotMatch\": \"Plugin Version Mismatch\",\n  \"error.allFieldsRequired\": \"All fields are required\",\n  \"error.archMismatch\": \"Chip architecture mismatch\",\n  \"error.avatarModelNotStarted\": \"Digital human model not started\",\n  \"error.cancelTaskFailed\": \"Cancel task failed\",\n  \"error.energyInsufficient\": \"Insufficient LLM energy, please recharge to continue using\",\n  \"error.fileExists\": \"File already exists\",\n  \"error.fileNotFound\": \"File not found\",\n  \"error.fileSelectFailed\": \"File selection failed\",\n  \"error.genCountRange\": \"Generate count must be between 1-10\",\n  \"error.loadRecordFailed\": \"Failed to load record {error}\",\n  \"error.maxSelection\": \"Can only select up to {count}\",\n  \"error.modelArchMismatch\": \"Model architecture mismatch\",\n  \"error.modelDirIdentifyFailed\": \"Model directory identification failed, please select the correct model directory\",\n  \"error.modelPathInvalid\": \"Model path cannot contain non-English characters, spaces, etc.\",\n  \"error.modelPlatformMismatch\": \"Model platform mismatch\",\n  \"error.modelServiceNotRunning\": \"Model service is not running\",\n  \"error.modelTypeInvalid\": \"Model type error\",\n  \"error.modelUnsigned\": \"Since the model file is not completely signed, please run the following command to complete the signature before running\",\n  \"error.modelVersionExists\": \"Model version already exists\",\n  \"error.nameDuplicate\": \"Name duplicate\",\n  \"error.noMicrophone\": \"No recording device detected\",\n  \"error.parseFailed\": \"Failed to parse return data\",\n  \"error.platformMismatch\": \"Platform mismatch\",\n  \"error.processError\": \"Processing error\",\n  \"error.processTimeout\": \"Processing timeout\",\n  \"error.recognitionModelNotStarted\": \"Voice recognition model not started\",\n  \"error.recognitionParamInvalid\": \"Voice recognition parameters incorrect\",\n  \"error.recordNotFound\": \"Record not found\",\n  \"error.requestError\": \"Request Error\",\n  \"error.requestFailed\": \"Request Failed\",\n  \"error.responseEmpty\": \"Response data is empty\",\n  \"error.resumeFailed\": \"Resume failed\",\n  \"error.saveFileFailed\": \"Save file failed: {error}\",\n  \"error.selectFileFailed\": \"Select file failed: {error}\",\n  \"error.softwareVersionMismatch\": \"Software does not meet model version requirements\",\n  \"error.soundAsrResultEmpty\": \"SoundAsr recognition result is empty, please check if the audio file is normal\",\n  \"error.soundGenerateResultEmpty\": \"SoundGenerate generation result is empty, please check if the parameters are correct\",\n  \"error.textToImageResultEmpty\": \"TextToImage generation result is empty, please check if the parameters are correct\",\n  \"error.imageToImageResultEmpty\": \"ImageToImage generation result is empty, please check if the parameters are correct\",\n  \"error.taskFailed\": \"Task failed\",\n  \"error.taskNotFound\": \"Task not found\",\n  \"error.getTaskFailed\": \"Failed to retrieve task\",\n  \"error.timbreNotFound\": \"Voice timbre not found\",\n  \"error.updateFailed\": \"Update failed\",\n  \"error.videoProcessFailed\": \"Video processing failed, please select another video\",\n  \"error.voiceModelNotStarted\": \"Voice model not started\",\n  \"error.voiceParamInvalid\": \"Voice synthesis parameters incorrect\",\n  \"feedback.anytime\": \"Feedback anytime if you encounter problems\",\n  \"feedback.help\": \"Encountered problems? Post for help\",\n  \"feedback.toolRequest\": \"Tool Request\",\n  \"fastPanel.shortcuts\": \"Quick Actions\",\n  \"form.inputField\": \"Please input {title}\",\n  \"form.required\": \"{title} is required\",\n  \"group.name\": \"Group Name\",\n  \"guide.audioReq1\": \"1. Please record in a quiet environment to avoid noise interference\",\n  \"guide.audioReq2\": \"2. Please use standard pronunciation, clear articulation, and appropriate speed\",\n  \"guide.audioReq3\": \"3. Recording duration is best controlled between 6-20 seconds, and should not exceed 20 seconds\",\n  \"guide.audioReq4\": \"4. After recording, listen to check if it meets the requirements before submitting\",\n  \"guide.videoReq1\": \"1. The video duration should be between 10 and 30 seconds, in MP4 format, and the recommended resolution is 1080p to 4K\",\n  \"guide.videoReq2\": \"2. To ensure the effect, the face must be exposed in every frame of the video, with no obstruction, and only one face should appear in the video\",\n  \"guide.videoReq3\": \"3. It is recommended that the person in the video keep their mouth closed or slightly open, the opening amplitude should not be too large, and keep a certain distance from the camera, which can be adjusted according to the synthesis effect\",\n  \"guide.videoReq4\": \"4. Do not keep your mouth closed the whole time, you can recite text like '1 2 3 4 5 6 7 8 9' in a normal tone loop\",\n  \"help.howToAddModel\": \"How to add a model?\",\n  \"home.welcome\": \"Welcome to\",\n  \"hotkey.doubleClick\": \" Double Click\",\n  \"hotkey.instructions\": \"Instructions: \",\n  \"hotkey.notSet\": \"Not Set\",\n  \"hotkey.step1\": \"① Click to activate\",\n  \"hotkey.step2Mac\": \"② Press modifier key (Control, Command, Option) first then press other keys, or quickly press modifier key twice\",\n  \"hotkey.step2Win\": \"② Press modifier key (Ctrl, Shift, Alt) first then press other keys, or quickly press modifier key twice\",\n  \"hint.addContent\": \"Please add content\",\n  \"hint.audioFormat\": \"Supports wav/mp3 format\",\n  \"hint.configPromptFirst\": \"Please configure prompt first\",\n  \"hint.fileTypes\": \"Supported file types\",\n  \"hint.inputContent\": \"Please input content\",\n  \"hint.inputKeywords\": \"Please input keywords\",\n  \"hint.inputName\": \"Please input name\",\n  \"hint.inputPreviewText\": \"Input preview text\",\n  \"hint.inputRandomText\": \"Please input random speech text\",\n  \"hint.inputRefText\": \"Please input reference text\",\n  \"hint.inputRequirement\": \"Please input your requirement\",\n  \"hint.inputStandardText\": \"Please input standard speech text\",\n  \"hint.inputSynthesisContent\": \"Please input synthesis content\",\n  \"hint.inputTagEnter\": \"Enter after inputting tag\",\n  \"hint.inputVoiceSynthesis\": \"Input voice content to start synthesis\",\n  \"hint.recordVoice\": \"Please record voice\",\n  \"hint.selectAudioFile\": \"Please select audio file\",\n  \"hint.selectAvatar\": \"Please select avatar\",\n  \"hint.selectAvatarModel\": \"Please select digital human model\",\n  \"hint.selectFileFormat\": \"Please select {extensions} format file\",\n  \"hint.selectModel\": \"Please select model\",\n  \"hint.selectModelCheck\": \"Please select model to check\",\n  \"hint.selectModelFirst\": \"Please select model first\",\n  \"hint.selectPlatform\": \"Please select model platform\",\n  \"hint.selectRecognitionModel\": \"Please select voice recognition model\",\n  \"hint.selectSynthesisType\": \"Please select synthesis type\",\n  \"hint.selectTimbre\": \"Please select timbre\",\n  \"hint.selectVideo\": \"Please select video\",\n  \"hint.selectVideoFile\": \"Please select video file\",\n  \"hint.selectVoice\": \"Please select voice\",\n  \"hint.selectVoiceModel\": \"Please select voice model\",\n  \"intro.interactionSupport\": \"Interactive communication supports major platforms\",\n  \"intro.lipSync\": \"Supports audio-driven lip sync replacement\",\n  \"intro.modelsSupported\": \"Supported by thousands of timbre models\",\n  \"intro.modelsUpdate\": \"Various open-source models continuously updated\",\n  \"intro.textToVideo\": \"Input text to automatically synthesize audio driving lip sync to synthesize video\",\n  \"intro.voiceClone\": \"Supports built-in voice synthesis, 5-second audio voice cloning\",\n  \"live.knowledge\": \"Live Knowledge\",\n  \"live.knowledgeUpdateHint\": \"Knowledge base updated, live data will be updated in 30 seconds\",\n  \"live.knowledgeUpdated\": \"Live knowledge base updated\",\n  \"live.noAvatarSelected\": \"No avatar selected for playback\",\n  \"live.noLoopMaterialSelected\": \"No loop material selected\",\n  \"live.setLiveRoomAddressFirst\": \"Please set live room address first\",\n  \"live.live\": \"Live\",\n  \"log.autoScroll\": \"Auto Scroll\",\n  \"log.view\": \"Log View\",\n  \"mcp.noTools\": \"No tools available\",\n  \"mcp.serverAddress\": \"MCP Server Address\",\n  \"media.audioFile\": \"Audio File\",\n  \"media.cropAudio\": \"Crop Audio\",\n  \"media.cropConfirm\": \"Confirm Crop\",\n  \"media.selectAudio\": \"Select Audio File\",\n  \"media.selectVideo\": \"Select Video File\",\n  \"media.subtitle\": \"Subtitle\",\n  \"media.subtitlePreview\": \"Subtitle Preview\",\n  \"media.video\": \"Video\",\n  \"monitor.debug\": \"Debug\",\n  \"monitor.refresh\": \"Refresh\",\n  \"model.accelerationOn\": \"Continuous call acceleration is on\",\n  \"model.embedModels\": \"Embedding Models\",\n  \"model.freeModels\": \"Free Models\",\n  \"model.builtinModels\": \"Built-in Models\",\n  \"model.testPrompt\": \"What model are you, brief answer\",\n  \"model.add\": \"Add Model\",\n  \"model.addCloud\": \"Add Cloud Model\",\n  \"model.addLocal\": \"Add Local Model\",\n  \"model.addProvider\": \"Add Provider\",\n  \"model.addSuccess\": \"Model added successfully\",\n  \"model.builtinDesc\": \"Built-in models can be used directly without configuration\",\n  \"model.censorResult\": \"Filtered word detection result\",\n  \"model.cloudAvatar\": \"Cloud Avatar\",\n  \"model.cloudModel\": \"Cloud Model\",\n  \"model.cloudModelDesc\": \"Cloud models support direct use, no need to download and install, convenient and fast\",\n  \"model.cloudModelService\": \"Cloud Model Service\",\n  \"model.cloudVideoAvatar\": \"Cloud Video Avatar\",\n  \"model.cloudVideoAvatarDesc\": \"Cloud video avatar, supports direct download to local use\",\n  \"model.deleteConfirm\": \"Are you sure you want to delete model {title} v{version}?\",\n  \"model.description\": \"Model Description\",\n  \"model.download\": \"Download Model\",\n  \"model.edit\": \"Edit Model\",\n  \"model.editProvider\": \"Edit Provider\",\n  \"model.hardwareReq\": \"Hardware Requirements\",\n  \"model.id\": \"Model ID\",\n  \"model.img2img\": \"Image-to-Image\",\n  \"model.info\": \"Model Info\",\n  \"model.list\": \"Model List\",\n  \"model.localModel\": \"Local Model\",\n  \"model.market\": \"Model Market\",\n  \"model.marketTip\": \"Visit Model Market, download model to local\",\n  \"model.model\": \"Model\",\n  \"model.name\": \"Model Name\",\n  \"model.notSupported\": \"Model Not Supported\",\n  \"model.runInCloudDesc\": \"Model runs in the cloud, avoiding local resource shortage\",\n  \"model.runInLocalDesc\": \"Model runs locally, requires computer performance\",\n  \"model.searchPlatform\": \"Search Model Platform\",\n  \"model.seedTip\": \"Click to generate random seed, same seed generates same result\",\n  \"model.select\": \"Select Model\",\n  \"model.selectLocal\": \"Select Local Model\",\n  \"model.signature\": \"Model File Signature\",\n  \"model.systemPrompt\": \"System Prompt\",\n  \"model.txt2img\": \"Text-to-Image\",\n  \"model.userPrompt\": \"User Prompt\",\n  \"model.unzipTip\": \"Unzip the model archive, select the config.json file in the directory\",\n  \"model.versionReq\": \"Version Requirement\",\n  \"msg.copiedToClipboard\": \"Copied {text} to clipboard\",\n  \"msg.fileSavedTo\": \"File saved to {path}\",\n  \"msg.moreContent\": \"For more content, please view\",\n  \"msg.moreTools\": \"Submit more tool requests to us\",\n  \"msg.passwordRequired\": \"Running process may require password input\",\n  \"msg.requestSuccessPlaying\": \"Request successful, start playing\",\n  \"msg.stopRequested\": \"Stop request sent, waiting for operation to stop\",\n  \"msg.videoProcessing\": \"Video processing may take a long time, please wait patiently\",\n  \"nav.apps\": \"Apps\",\n  \"nav.feedback\": \"Feedback\",\n  \"nav.guide\": \"Guide\",\n  \"nav.home\": \"Home\",\n  \"nav.log\": \"Log\",\n  \"nav.toolbox\": \"Toolbox\",\n  \"nav.userCenter\": \"User Center\",\n  \"payment.error\": \"Error occurred\",\n  \"payment.expired\": \"Expired\",\n  \"payment.paidClosing\": \"Paid, closing soon\",\n  \"payment.payWithinSeconds\": \"Pay within {seconds} seconds\",\n  \"payment.qrcodeExpired\": \"QR Code Expired\",\n  \"payment.scanQRCode\": \"Scan with WeChat / Alipay\",\n  \"payment.scanned\": \"Scanned\",\n  \"placeholder.chatgpt\": \"e.g. ChatGPT\",\n  \"placeholder.gpt35\": \"e.g. GPT-3.5\",\n  \"placeholder.requiredGpt\": \"Required, e.g. gpt-3.5-turbo\",\n  \"plugin.autoDetachWindow\": \"Auto Detach to Independent Window\",\n  \"plugin.backendLog\": \"Plugin Backend Log\",\n  \"plugin.debugWindow\": \"Plugin Debug Window\",\n  \"plugin.disabled\": \"Disabled\",\n  \"plugin.enabled\": \"Enabled\",\n  \"plugin.installFailed\": \"Install Failed\",\n  \"plugin.installLocalConfig\": \"Select Plugin config.json\",\n  \"plugin.installLocalZip\": \"Select Local ZIP Plugin\",\n  \"plugin.installSuccess\": \"Install Success\",\n  \"plugin.localPlugin\": \"Local Plugin:{path}\",\n  \"plugin.market\": \"Plugin Market\",\n  \"plugin.notFound\": \"No plugins found\",\n  \"plugin.publish\": \"Publish Plugin\",\n  \"plugin.publishFailed\": \"Publish failed:{error}\",\n  \"plugin.publishSuccess\": \"Publish successful\",\n  \"plugin.publishing\": \"Publishing\",\n  \"plugin.refreshSuccess\": \"Refresh successful\",\n  \"plugin.search\": \"Search Plugin\",\n  \"plugin.uninstall\": \"Uninstall\",\n  \"plugin.uninstallConfirm\": \"Are you sure you want to uninstall the plugin?\",\n  \"plugin.uninstallFailed\": \"Uninstall failed:{error}\",\n  \"plugin.uninstallSuccess\": \"Uninstall successful\",\n  \"plugin.updateInfo\": \"Update Info\",\n  \"plugin.updateInfoFailed\": \"Update Info Failed\",\n  \"plugin.updateInfoSuccess\": \"Update Info Success\",\n  \"plugin.updatingInfo\": \"Updating Info\",\n  \"proUpgrade.defaultDesc\": \"Please download Pro version to unlock full features\",\n  \"proUpgrade.downloadButton\": \"Download Pro Version\",\n  \"proUpgrade.title\": \"Upgrade Feature Tip\",\n  \"provider.baichuan\": \"Baichuan\",\n  \"provider.baiduCloud\": \"Baidu Cloud Qianfan\",\n  \"provider.buildIn\": \"Built-in Models\",\n  \"provider.dashscope\": \"Alibaba Cloud Bailian\",\n  \"provider.deepseek\": \"DeepSeek\",\n  \"provider.doubao\": \"Volcengine\",\n  \"provider.hunyuan\": \"Tencent Hunyuan\",\n  \"provider.infini\": \"Infini-AI\",\n  \"provider.modelscope\": \"ModelScope\",\n  \"provider.moonshot\": \"Moonshot AI\",\n  \"provider.nvidia\": \"NVIDIA\",\n  \"provider.ppio\": \"PPIO\",\n  \"provider.silicon\": \"Silicon Flow\",\n  \"provider.stepfun\": \"StepFun\",\n  \"provider.tencentCloudTi\": \"Tencent Cloud TI\",\n  \"provider.xirang\": \"Tianyiyun Xirang\",\n  \"provider.yi\": \"Yi (01.AI)\",\n  \"provider.zhinao\": \"360 AI\",\n  \"provider.zhipu\": \"Zhipu AI\",\n  \"service.start\": \"Start Service\",\n  \"service.startFailed\": \"Start Failed\",\n  \"service.starting\": \"Starting\",\n  \"setting.altDoubleClick\": \"Alt Double Click\",\n  \"setting.altSingleClick\": \"Alt Single Click\",\n  \"setting.apiKey\": \"API Key\",\n  \"setting.apiUrl\": \"API URL\",\n  \"setting.askEveryTime\": \"Ask Every Time\",\n  \"setting.autoLaunch\": \"Launch at Startup\",\n  \"setting.autoStart\": \"Auto Start\",\n  \"setting.autoUpdate\": \"Auto Check Update\",\n  \"setting.basic\": \"Basic Settings\",\n  \"setting.commandDoubleClick\": \"Command Double Click\",\n  \"setting.commandSingleClick\": \"Command Single Click\",\n  \"setting.controlDoubleClick\": \"Control Double Click\",\n  \"setting.controlSingleClick\": \"Control Single Click\",\n  \"setting.ctrlDoubleClick\": \"Ctrl Double Click\",\n  \"setting.ctrlSingleClick\": \"Ctrl Single Click\",\n  \"setting.cudaAcceleration\": \"CUDA Acceleration\",\n  \"setting.dataConfig\": \"Data Config\",\n  \"setting.detachWindowHotkey\": \"Detach Window Hotkey\",\n  \"setting.env\": \"Environment Settings\",\n  \"setting.exitDirectly\": \"Exit Directly\",\n  \"setting.fixCommand\": \"Fix Command\",\n  \"setting.followSystem\": \"Follow System\",\n  \"setting.interfaceType\": \"Interface Type\",\n  \"setting.llm\": \"LLM Settings\",\n  \"setting.localModelDir\": \"Local Model Dir\",\n  \"setting.onClose\": \"On Close\",\n  \"setting.optionDoubleClick\": \"Option Double Click\",\n  \"setting.optionSingleClick\": \"Option Single Click\",\n  \"setting.pathChangeConfirm\": \"Confirm change storage path to {path}?\",\n  \"setting.pathChangeRestart\": \"Changing storage path requires restarting software\",\n  \"setting.storagePath\": \"Storage Path\",\n  \"setting.themeStyle\": \"Theme Style\",\n  \"setting.triggerType\": \"Trigger Type\",\n  \"setting.wpm\": \"Words Per Minute\",\n  \"setup.allCompleted\": \"All setup completed\",\n  \"setup.congratulations\": \"Congratulations on completing {title} setup\",\n  \"setup.openSettings\": \"Open Settings\",\n  \"setup.verifyComplete\": \"Verify Complete\",\n  \"soundAsr.copyResult\": \"Copy Recognition Result\",\n  \"soundAsrEdit.inputSearchContent\": \"Please input search content\",\n  \"soundAsrEdit.invalidTimeRange\": \"Invalid time range, must be within the record\",\n  \"soundAsrEdit.mergedBlankSegments\": \"Merged continuous blank segments\",\n  \"soundAsrEdit.mergeOnlyContinuous\": \"Can only merge continuous records\",\n  \"soundAsrEdit.noEditRecord\": \"No edit record\",\n  \"soundAsrEdit.noMatchFound\": \"No match found\",\n  \"soundAsrEdit.optimizeComplete\": \"Optimization completed, successfully fixed {successCount} sentences, failed {failCount} sentences\",\n  \"soundAsrEdit.replacedRecords\": \"Replaced {count} records\",\n  \"soundAsrEdit.time\": \"Time\",\n  \"soundReplace.confirmComplete\": \"Confirm Completed\",\n  \"soundReplace.confirmText\": \"Confirm Text\",\n  \"soundReplace.confirmTextDesc\": \"Check and confirm the recognized text content\",\n  \"soundReplace.extractAndRecognize\": \"Extract and Recognize Audio\",\n  \"soundReplace.extractAndRecognizeDesc\": \"Select video file containing voice to replace\",\n  \"soundReplace.extractAudio\": \"Extract Audio\",\n  \"soundReplace.manualConfirmText\": \"Manually Confirm Text\",\n  \"soundReplace.modifyText\": \"Modify Text\",\n  \"soundReplace.reorderConfirm\": \"Reorder Confirm\",\n  \"soundReplace.reverifyText\": \"Reverify Text\",\n  \"soundReplace.saveAndSynthesize\": \"Save and Synthesize\",\n  \"soundReplace.submitTask\": \"Submit Task\",\n  \"soundReplace.synthesizeReplace\": \"Synthesize and Replace Voice\",\n  \"soundReplace.synthesizeReplaceDesc\": \"Set voice synthesis model parameters to generate new speech\",\n  \"soundReplace.taskSubmitted\": \"Task submitted\",\n  \"soundReplace.videoSynthesis\": \"Video Synthesis\",\n  \"status.cancelling\": \"Cancelling\",\n  \"status.deleting\": \"Deleting\",\n  \"status.downloading\": \"Downloading\",\n  \"status.downloadingProgress\": \"Downloading {index}/{total}\",\n  \"status.loading\": \"Loading\",\n  \"status.manuallyCompleted\": \"Manually Completed\",\n  \"status.notRunning\": \"Not Running\",\n  \"status.queuing\": \"Queuing\",\n  \"status.resuming\": \"Resuming\",\n  \"status.retrying\": \"Retrying\",\n  \"status.running\": \"Running\",\n  \"status.startedTime\": \"Started {time}\",\n  \"status.stopped\": \"Stopped\",\n  \"status.submitting\": \"Submitting\",\n  \"status.unprocessed\": \"Unprocessed\",\n  \"status.waiting\": \"Waiting\",\n  \"subtitleTts.audioSynthesis\": \"Audio Synthesis\",\n  \"subtitleTts.parseSubtitle\": \"Parse Subtitle\",\n  \"subtitleTts.settings\": \"Subtitle to Audio Settings\",\n  \"subtitleTts.synthesizedAudio\": \"Synthesized Audio\",\n  \"store.searchPlaceholder\": \"Enter keywords to search\",\n  \"system.actionManagement\": \"Action Management\",\n  \"system.addFileLaunch\": \"Add a File Launch\",\n  \"system.aiModel\": \"AI Models\",\n  \"system.dataCenter\": \"Data Center\",\n  \"system.fileLaunch\": \"File Launch\",\n  \"system.functionSettings\": \"Function Settings\",\n  \"system.hotkeys\": \"Hotkeys\",\n  \"system.myAccount\": \"My Account\",\n  \"system.personalCenter\": \"Personal Center\",\n  \"system.pluginManagement\": \"Plugin Management\",\n  \"system.preferences\": \"Preferences\",\n  \"task.batchTextSynthesis\": \"Batch Text Synthesis\",\n  \"task.cancel\": \"Cancel Task\",\n  \"task.cancelled\": \"Task Cancelled\",\n  \"task.cloneSubmitted\": \"Task submitted successfully, waiting for cloning\",\n  \"task.details\": \"Task Details\",\n  \"task.editResult\": \"Edit Result\",\n  \"task.longTextToAudio\": \"Long Text to Audio\",\n  \"task.oneClickRun\": \"One Click Run\",\n  \"task.optimizeTimeline\": \"One Click Optimize Timeline\",\n  \"task.processing\": \"Processing\",\n  \"task.recognitionSubmitted\": \"Voice recognition task submitted\",\n  \"task.resume\": \"Resume Task\",\n  \"task.retry\": \"Retry Task\",\n  \"task.startRecognition\": \"Start Recognition\",\n  \"task.startSynthesis\": \"Start Synthesis\",\n  \"task.startVideoGen\": \"Start Video Generation\",\n  \"task.submitSynthesis\": \"Submit Synthesis\",\n  \"task.subtitleToAudio\": \"Subtitle to Audio\",\n  \"task.synthesisType\": \"Synthesis Type\",\n  \"task.synthesize\": \"Synthesize\",\n  \"task.videoGenSubmitted\": \"Task submitted successfully, waiting for video generation\",\n  \"task.view\": \"View Task\",\n  \"theme.dark\": \"Dark\",\n  \"theme.light\": \"Light\",\n  \"time.hour\": \"Hour\",\n  \"time.minute\": \"Minute\",\n  \"time.second\": \"Second\",\n  \"update.alreadyLatest\": \"Already latest version\",\n  \"update.check\": \"Check Update\",\n  \"update.checkFailed\": \"Check Update Failed\",\n  \"update.newVersionFound\": \"New version {version} found, download and update now?\",\n  \"user.comment\": \"User Comment\",\n  \"user.energy\": \"Energy\",\n  \"user.enter\": \"User Enter\",\n  \"user.knowledge\": \"User Knowledge\",\n  \"user.like\": \"User Like\",\n  \"user.name\": \"Username\",\n  \"user.reward\": \"User Reward\",\n  \"video.contentVideo\": \"Content Video\",\n  \"video.loopBroadcast\": \"Loop Broadcast\",\n  \"video.loopContent\": \"Loop Content Video\",\n  \"video.providerName\": \"Provider Name\",\n  \"voice.add\": \"Add Voice\",\n  \"voice.clone\": \"Voice Clone\",\n  \"voice.cloneModel\": \"Voice Clone Model\",\n  \"voice.config\": \"Voice Config\",\n  \"voice.crossLanguage\": \"Cross Language\",\n  \"voice.currentConfig\": \"Current Voice Config\",\n  \"voice.file\": \"Voice File\",\n  \"voice.recognition\": \"Voice Recognition\",\n  \"voice.recognitionConfig\": \"Voice Recognition Config\",\n  \"voice.recognitionModel\": \"Voice Recognition Model\",\n  \"voice.record\": \"Record Audio\",\n  \"voice.refAudioGuide1\": \"Reference audio controlled within 6-20s to ensure audio clarity\",\n  \"voice.refAudioGuide2\": \"Reference audio needs to be > 6s and < 20s to ensure clarity\",\n  \"voice.refTextRequired\": \"Reference audio full text content required, some models need it\",\n  \"voice.referenceAudio\": \"Reference Audio\",\n  \"voice.referenceText\": \"Reference Text\",\n  \"voice.replace\": \"Voice Replace\",\n  \"voice.replaceConfig\": \"Voice Replace Config\",\n  \"voice.rerecord\": \"Re-record\",\n  \"voice.select\": \"Select Voice\",\n  \"voice.selectFile\": \"Select Voice File\",\n  \"voice.selectTimbre\": \"Select Timbre\",\n  \"voice.synthesis\": \"Voice Synthesis\",\n  \"voice.synthesisConfig\": \"Voice Synthesis Config\",\n  \"voice.synthesisModel\": \"Voice Synthesis Model\",\n  \"voice.timbre\": \"Voice Timbre\",\n  \"voice.timbreDesc\": \"Timbre Description\",\n  \"voice.timbreManage\": \"Timbre Manage\",\n  \"voice.voice\": \"Voice\",\n  \"welcome.title\": \"Welcome to AIGCPanel!\",\n  \"workflow.create\": \"Create Workflow\",\n  \"workflow.workflow\": \"Workflow\",\n  \"workflow.configureRecognitionAndGeneration\": \"Please configure voice recognition and voice generation services\",\n  \"workflow.inputVideoParam\": \"Please input video parameter\",\n  \"workflow.paramErrorMissing\": \"Parameter error: missing {items}\",\n  \"workflow.soundGenerationService\": \"Voice generation service\",\n  \"workflow.soundRecognitionService\": \"Voice recognition service\",\n  \"about.devModeSettings\": \"Dev Mode Settings\",\n  \"about.fastPanelHideOnBlur\": \"Fast Panel Hide on Blur\",\n  \"action.backendCode\": \"Backend Code\",\n  \"action.code\": \"Code\",\n  \"action.command\": \"Command\",\n  \"action.smartArea\": \"Smart Area\",\n  \"action.webpage\": \"Webpage\",\n  \"backup.connectFailed\": \"Connection Failed\",\n  \"backup.fileFormat\": \"File Format\",\n  \"backup.notConfigured\": \"WebDav not configured, click to configure\",\n  \"backup.password\": \"Password\",\n  \"backup.placeholderSupport\": \"Placeholder Support\",\n  \"backup.rootDir\": \"Root Directory\",\n  \"backup.selectFile\": \"Select file to restore\",\n  \"backup.selectRestoreFile\": \"Please select a file to restore\",\n  \"backup.startBackup\": \"Start Backup\",\n  \"backup.startRestore\": \"Start Restore\",\n  \"backup.uploadToCloud\": \"Upload to Cloud\",\n  \"backup.restoreFromCloud\": \"Restore from Cloud\",\n  \"backup.username\": \"Username\",\n  \"backup.webdavConfig\": \"Config\",\n  \"backup.webdavSettings\": \"WebDav Settings\",\n  \"data.backupRestore\": \"Backup/Restore\",\n  \"data.clear\": \"Clear\",\n  \"data.clearConfirm\": \"Are you sure to clear all?\",\n  \"data.clearSuccess\": \"Cleared successfully\",\n  \"data.deleteConfirm\": \"Are you sure to delete?\",\n  \"data.deleteSuccess\": \"Deleted successfully\",\n  \"data.docCount\": \"documents\",\n  \"data.filterPlaceholder\": \"Type keyword to filter\",\n  \"data.title\": \"Data Center\",\n  \"launch.actionName\": \"Action name, e.g. Screenshot\",\n  \"launch.addHotkey\": \"Add Hotkey\",\n  \"launch.custom\": \"Custom\",\n  \"launch.enterActionName\": \"Please enter action name\",\n  \"launch.hotkey\": \"Hotkey\",\n  \"log.noLogs\": \"No Log Files\",\n  \"log.openFile\": \"Open File\",\n  \"main.clearAllConfirm\": \"Confirm clear all?\",\n  \"main.expandAll\": \"Expand All\",\n  \"main.loading\": \"Loading\",\n  \"main.matchResults\": \"Match Results\",\n  \"main.multipleFiles\": \"Multiple Files\",\n  \"main.multipleFolders\": \"Multiple Folders\",\n  \"main.multipleImages\": \"Multiple Images\",\n  \"main.pinned\": \"Pinned\",\n  \"main.placeholder\": \"FocusAny, make your work focused and efficient\",\n  \"main.recentlyUsed\": \"Recently Used\",\n  \"main.runError\": \"An error occurred during execution\",\n  \"main.searchResults\": \"Search Results\",\n  \"main.starting\": \"Starting\",\n  \"main.window\": \"Window\",\n  \"plugin.detachWindow\": \"Open in Detached Window\",\n  \"plugin.actionNotFound\": \"Action not found, please check keywords or action name\",\n  \"plugin.colorCopied\": \"Color {color} copied to clipboard\",\n  \"plugin.colorCopyShortcut\": \"Copy {shortcut}\",\n  \"plugin.errorLog\": \"Plugin {name} error: {error}\",\n  \"plugin.exitEsc\": \"Exit ESC\",\n  \"plugin.installComplete\": \"Plugin {title} installed successfully\",\n  \"plugin.installing\": \"Installing plugin\",\n  \"plugin.newWindow\": \"New Window\",\n  \"plugin.noPermission\": \"Plugin has no permission ({permission})\",\n  \"plugin.opening\": \"Opening plugin\",\n  \"plugin.notExist\": \"Plugin {name} not found\",\n  \"plugin.screenshotHint\": \"Please use the screenshot tool\",\n  \"plugin.selectSavePath\": \"Select save path\",\n  \"editor.noPluginForFile\": \"No plugin found to open this file\",\n  \"file.notFoundOrReadFailed\": \"File not found or read failed\",\n  \"file.unsupportedType\": \"Unsupported file type\",\n  \"screenshot.edit\": \"Screenshot Editor\",\n  \"system.title\": \"System Settings\",\n  \"system.desc\": \"Provides basic system functions\",\n  \"system.apps\": \"Applications\",\n  \"system.appsDesc\": \"Search and open system applications\",\n  \"system.appsIndexed\": \"Application indexing complete\",\n  \"system.appsIndexing\": \"Analyzing applications, search will be available soon...\",\n  \"system.fileLaunchDesc\": \"One-click file launch\",\n  \"system.workflowDesc\": \"Workflow management\",\n  \"system.storeDesc\": \"Plugin market management\",\n  \"system.about\": \"About Us\",\n  \"system.screenshot\": \"Screenshot\",\n  \"system.colorPicker\": \"Color Picker\",\n  \"system.screenRecord\": \"Screen Recording\",\n  \"system.lockScreen\": \"Lock Screen\",\n  \"system.lanIP\": \"LAN IP\",\n  \"system.ipCopied\": \"IP address {ip} copied to clipboard\",\n  \"tray.visitWebsite\": \"Visit Website\",\n  \"debug.info\": \"Debug Info\",\n  \"debug.copyRoute\": \"Copy Route\",\n  \"setting.darkTheme\": \"Dark\",\n  \"setting.fastPanel\": \"Fast Panel\",\n  \"setting.fastPanelHotkey\": \"Fast Panel Hotkey\",\n  \"setting.functionSettings\": \"Function Settings\",\n  \"setting.invokeHotkey\": \"Invoke Hotkey\",\n  \"setting.language\": \"Interface Language\",\n  \"setting.lightTheme\": \"Light\"\n}\n"
  },
  {
    "path": "src/lang/index.ts",
    "content": "import { createI18n } from \"vue-i18n\";\n\nimport enUS from \"./en-US.json\";\nimport zhCN from \"./zh-CN.json\";\n\nlet localeInit = false;\nexport const defaultLocale = \"zh-CN\";\n\nexport const messageList = [\n    {\n        name: \"en-US\",\n        label: \"English\",\n        messages: enUS,\n    },\n    {\n        name: \"zh-CN\",\n        label: \"简体中文\",\n        messages: zhCN,\n    },\n];\n\nconst buildMessages = (): any => {\n    let messages = {};\n    for (let m of messageList) {\n        messages[m.name] = m.messages;\n    }\n    return messages;\n};\n\nconst messages = buildMessages();\n\nexport const i18n = createI18n({\n    locale: defaultLocale,\n    legacy: false,\n    globalInjection: true,\n    messages,\n});\n\nif (typeof window !== \"undefined\" && window.$mapi) {\n    window.$mapi.config.get(\"lang\", defaultLocale).then((lang: string) => {\n        i18n.global.locale.value = lang as any;\n        localeInit = true;\n        fireLocaleChange(lang);\n    });\n}\n\nexport type LocaleItem = {\n    name: string;\n    label: string;\n    active?: boolean;\n};\n\nexport const listLocales = () => {\n    let list: LocaleItem[] = messageList;\n    list.forEach((item) => {\n        item.active = i18n.global.locale.value === item.name;\n    });\n    return list;\n};\n\nexport const getLocale = async () => {\n    return new Promise<string>((resolve) => {\n        if (localeInit) {\n            resolve(i18n.global.locale.value);\n        } else {\n            setTimeout(() => {\n                resolve(getLocale());\n            }, 100);\n        }\n    });\n};\n\nlet localeChangeListener: Array<(locale: string) => void> = [];\n\nexport const onLocaleChange = (callback: (lang: string) => void) => {\n    localeChangeListener.push(callback);\n};\n\nconst fireLocaleChange = (lang: string) => {\n    localeChangeListener.forEach((callback) => {\n        callback(lang);\n    });\n};\n\nexport const changeLocale = (lang: string) => {\n    i18n.global.locale.value = lang as any;\n    window.$mapi.config.set(\"lang\", lang).then(() => {\n        fireLocaleChange(lang);\n    });\n};\n\nexport const applyLocale = (lang: string) => {\n    i18n.global.locale.value = lang as any;\n    fireLocaleChange(lang);\n};\n\nexport const t = (key: string, param: object | null = null) => {\n    // check if exists key\n    if (!(key in messages[i18n.global.locale.value])) {\n        if (param) {\n            return key.replace(/\\{(\\w+)\\}/g, function (match, key) {\n                return key in param ? param[key] : match;\n            });\n        }\n        return key;\n    }\n    // @ts-ignore\n    return i18n.global.t(key, param as any);\n};\n"
  },
  {
    "path": "src/lang/zh-CN.json",
    "content": "{\n  \"config.slogan\": \"专注提效的AI工具条\",\n  \"model.testPrompt\": \"你是什么模型，简短回答\",\n  \"about.disclaimer\": \"声明\",\n  \"about.license\": \"本产品为开源软件，遵循 AGPL-3.0 license 协议。\",\n  \"about.software\": \"关于软件\",\n  \"about.title\": \"关于\",\n  \"action.attrMatch\": \"属性匹配\",\n  \"action.builtin\": \"内置\",\n  \"action.fileExtensions\": \"文件后缀\",\n  \"action.fileType\": \"文件类型\",\n  \"action.matchAction\": \"匹配动作\",\n  \"action.matchEditorHint\": \"当文件匹配到以下后缀和类型时\",\n  \"action.matchExactKeyHint\": \"输入完全等于以下关键词\",\n  \"action.matchFileHint\": \"当使用以下规则匹配成功时\",\n  \"action.matchImageHint\": \"当匹配到图片时\",\n  \"action.matchKeywordHint\": \"输入匹配以下关键词，包含全拼、首字母简写\",\n  \"action.matchRegexHint\": \"输入匹配以下正则表达式\",\n  \"action.matchWindowHint\": \"当激活窗口匹配以下条件成功时\",\n  \"action.maxCount\": \"最大数量\",\n  \"action.minCount\": \"最小数量\",\n  \"action.nameMatch\": \"名称匹配\",\n  \"action.pinToSearch\": \"固定到搜索框\",\n  \"action.plugin\": \"插件\",\n  \"action.regex\": \"正则\",\n  \"action.searchAction\": \"搜索动作\",\n  \"action.suffix\": \"后缀\",\n  \"action.titleMatch\": \"标题匹配\",\n  \"action.unpinFromSearch\": \"从搜索框取消固定\",\n  \"action.window\": \"窗口\",\n  \"avatar.addVideo\": \"添加视频形象\",\n  \"avatar.audioToVideo\": \"音频驱动口型合成视频\",\n  \"avatar.avatar\": \"数字人形象\",\n  \"avatar.canOpenCloseMouth\": \"可张口闭口\",\n  \"avatar.config\": \"数字人配置\",\n  \"avatar.digitalHuman\": \"数字人\",\n  \"avatar.example\": \"形象示例\",\n  \"avatar.faceInterference\": \"面部有干扰\",\n  \"avatar.live\": \"数字人直播\",\n  \"avatar.manage\": \"管理多个数字人形象\",\n  \"avatar.model\": \"数字人模型\",\n  \"avatar.oneClickSynthesis\": \"数字人一键合成\",\n  \"avatar.saveToMine\": \"保存到我的形象\",\n  \"avatar.selfie\": \"正脸自拍\",\n  \"avatar.smartLive\": \"智能直播\",\n  \"avatar.synthesis\": \"数字人合成\",\n  \"avatar.video\": \"视频形象\",\n  \"avatar.videoReq\": \"形象视频要求\",\n  \"backup.backingUp\": \"正在备份...\",\n  \"backup.backupFailed\": \"备份失败\",\n  \"backup.backupSuccess\": \"备份成功\",\n  \"backup.backupToFile\": \"备份为文件\",\n  \"backup.backupToLocal\": \"备份到本地\",\n  \"backup.formatTip\": \"备份采用 backup 格式，定期备份文件可避免数据丢失。\",\n  \"backup.restoreFromFile\": \"从文件恢复\",\n  \"backup.restoreFromLocal\": \"从本地恢复\",\n  \"backup.restoreFailed\": \"恢复失败\",\n  \"backup.restoreSuccess\": \"恢复成功\",\n  \"backup.restoring\": \"正在恢复...\",\n  \"backup.title\": \"备份/恢复\",\n  \"common.adapt\": \"适配\",\n  \"common.add\": \"添加\",\n  \"common.addFile\": \"添加文件\",\n  \"common.addOne\": \"添加一个\",\n  \"common.aiGenerated\": \"AI生成\",\n  \"common.all\": \"全部\",\n  \"common.and\": \"和\",\n  \"common.availableVars\": \"可用变量\",\n  \"common.back\": \"返回\",\n  \"common.batchInput\": \"批量输入\",\n  \"common.batchPaste\": \"批量粘贴\",\n  \"common.batchPasteHint\": \"批量粘贴，每行一个\",\n  \"common.cancel\": \"取消\",\n  \"common.check\": \"检查\",\n  \"common.clearConfirm\": \"确认清空？\",\n  \"common.clearHistory\": \"清空历史\",\n  \"common.clickTextToCopy\": \"点击文字复制\",\n  \"common.clickToConfig\": \"点击配置\",\n  \"common.clickToCopy\": \"点击复制\",\n  \"common.close\": \"关闭\",\n  \"common.collapse\": \"收起\",\n  \"common.confirm\": \"确定\",\n  \"common.copySuccess\": \"复制成功\",\n  \"common.copyText\": \"复制文本\",\n  \"common.default\": \"默认\",\n  \"common.disable\": \"禁用\",\n  \"common.enable\": \"启用\",\n  \"common.open\": \"打开\",\n  \"common.delete\": \"删除\",\n  \"common.deleteConfirm\": \"确认删除？\",\n  \"common.deleteRecordsConfirm\": \"确定删除 {count} 条记录?\",\n  \"common.description\": \"说明\",\n  \"common.detail\": \"详情\",\n  \"common.docs\": \"文档\",\n  \"common.download\": \"下载\",\n  \"common.downloadFailed\": \"下载失败\",\n  \"common.downloadSuccess\": \"下载成功\",\n  \"common.downloadingWait\": \"下载中，请耐心等待\",\n  \"common.duration\": \"时长\",\n  \"common.edit\": \"编辑\",\n  \"common.error\": \"错误\",\n  \"common.exit\": \"退出\",\n  \"common.exitConfirm\": \"确定退出软件？\",\n  \"common.expand\": \"展开\",\n  \"common.extensions\": \"{extensions}\",\n  \"common.failed\": \"失败\",\n  \"common.feature\": \"功能\",\n  \"common.file\": \"文件\",\n  \"common.find\": \"查找\",\n  \"common.folder\": \"文件夹\",\n  \"common.generate\": \"生成\",\n  \"common.generateFailed\": \"生成失败\",\n  \"common.hideWindow\": \"隐藏窗口\",\n  \"common.image\": \"图片\",\n  \"common.info\": \"信息\",\n  \"common.inputContent\": \"输入内容\",\n  \"common.key\": \"键\",\n  \"common.keywords\": \"关键词\",\n  \"common.language\": \"语言\",\n  \"common.loading\": \"加载中\",\n  \"common.loadingDots\": \"加载中...\",\n  \"common.localFile\": \"本地文件\",\n  \"common.loginRequired\": \"请先登录\",\n  \"common.merge\": \"合并\",\n  \"common.mergeWhitespace\": \"合并空白\",\n  \"common.modify\": \"修改\",\n  \"common.more\": \" 更多\",\n  \"common.moreDetails\": \"获取更多详情\",\n  \"common.name\": \"名称\",\n  \"common.no\": \"否\",\n  \"common.none\": \"无\",\n  \"common.notLoggedIn\": \"未登录\",\n  \"common.officialSite\": \"官网\",\n  \"common.onlineDocs\": \"在线文档\",\n  \"common.openFile\": \"打开文件\",\n  \"common.openPath\": \"打开路径\",\n  \"common.pause\": \"暂停\",\n  \"common.pro\": \"Pro\",\n  \"common.recharge\": \"充值\",\n  \"common.refresh\": \"刷新\",\n  \"common.rememberChoice\": \"记住我的选择\",\n  \"common.replace\": \"替换\",\n  \"common.reselect\": \"重新选择\",\n  \"common.resolution\": \"分辨率\",\n  \"common.restoreDefault\": \"恢复默认\",\n  \"common.resumeSuccess\": \"继续成功\",\n  \"common.retryAttempt\": \"继续尝试\",\n  \"common.retryFailed\": \"重试失败\",\n  \"common.retrySuccess\": \"重试成功\",\n  \"common.save\": \"保存\",\n  \"common.saveSuccess\": \"保存成功\",\n  \"common.select\": \"选择\",\n  \"common.selectAll\": \"全选\",\n  \"common.selectFile\": \"选择文件\",\n  \"common.selectLocalFile\": \"选择本地文件\",\n  \"common.selectPath\": \"选择路径\",\n  \"common.sendFailed\": \"发送失败\",\n  \"common.sendSuccess\": \"发送成功\",\n  \"common.service\": \"服务\",\n  \"common.setting\": \"设置\",\n  \"common.settingSuccess\": \"设置成功\",\n  \"common.split\": \"分割\",\n  \"common.stopFailed\": \"停止失败\",\n  \"common.stopService\": \"停止服务\",\n  \"common.stopping\": \"停止中\",\n  \"common.submitConfirm\": \"确认提交\",\n  \"common.success\": \"成功\",\n  \"common.system\": \"系统\",\n  \"common.tag\": \"标签\",\n  \"common.test\": \"测试\",\n  \"common.testFailed\": \"测试失败\",\n  \"common.testSuccess\": \"测试成功\",\n  \"common.testing\": \"测试中，请稍候...\",\n  \"common.tip\": \"提示\",\n  \"common.totalCount\": \"共 {count} 条\",\n  \"common.totalWords\": \"共{count}字\",\n  \"common.type\": \"类型\",\n  \"common.useNow\": \"立即使用\",\n  \"common.value\": \"值\",\n  \"common.version\": \"版本\",\n  \"common.view\": \"查看\",\n  \"common.viewCode\": \"代码查看\",\n  \"common.viewEffect\": \"效果查看\",\n  \"common.viewRecord\": \"记录查看\",\n  \"common.vipRequired\": \"请先开通会员\",\n  \"common.yes\": \"是\",\n  \"dashboard.statistics\": \"数据统计\",\n  \"dashboard.today\": \"今日\",\n  \"dashboard.todayTotalTasks\": \"今日总任务\",\n  \"desc.img2img\": \"根据输入图片+描述提示生成新的图片\",\n  \"desc.longTextToAudio\": \"将长文本内容转换为音频文件\",\n  \"desc.recognitionDownload\": \"识别文件下载文本/字幕\",\n  \"desc.recognitionEdit\": \"识别音频文件，支持识别后编辑/下载文本/字幕文件\",\n  \"desc.subtitleToAudio\": \"将字幕文件转换为音频文件\",\n  \"desc.txt2img\": \"根据文本描述生成图片\",\n  \"desc.videoVoiceReplace\": \"将视频中的人声替换为其他音色\",\n  \"download.audio\": \"下载音频\",\n  \"download.subtitleFile\": \"下载字幕文件\",\n  \"download.textFile\": \"下载文本文件\",\n  \"empty.noDownloadRecord\": \"没有可以下载的记录\",\n  \"empty.noEditableData\": \"没有可编辑的数据\",\n  \"empty.noLocalModel\": \"没有可用本地模型\",\n  \"empty.noLog\": \"暂无日志\",\n  \"empty.noLogFile\": \"暂无日志文件\",\n  \"empty.noModel\": \"没有可用模型\",\n  \"empty.noModelAdd\": \"暂时还没有模型，请添加模型~\",\n  \"empty.noModelPlatform\": \"没有找到相关模型平台\",\n  \"empty.noRecognitionTask\": \"暂无语音识别任务\",\n  \"empty.noRecord\": \"没有可用记录\",\n  \"empty.noVoiceTask\": \"暂无声音合成任务\",\n  \"error.pluginAlreadyExists\": \"插件已存在\",\n  \"error.pluginEditionNotMatch\": \"FocusAny类型不满足插件要求\",\n  \"error.pluginFormatError\": \"插件格式错误\",\n  \"error.pluginNotExists\": \"插件不存在\",\n  \"error.pluginNotSupportPlatform\": \"插件不支持当前平台\",\n  \"error.pluginReleaseDocFormatError\": \"插件release文档格式错误\",\n  \"error.pluginReleaseDocNotFound\": \"插件release文档不存在\",\n  \"error.pluginVersionNotMatch\": \"FocusAny版本不满足插件要求\",\n  \"error.publishVersionNotMatch\": \"插件版本不匹配\",\n  \"error.allFieldsRequired\": \"所有内容不能为空\",\n  \"error.archMismatch\": \"芯片架构不匹配\",\n  \"error.avatarModelNotStarted\": \"数字人模型未启动\",\n  \"error.cancelTaskFailed\": \"取消任务失败\",\n  \"error.energyInsufficient\": \"大模型能量不足，请充值后继续使用\",\n  \"error.fileExists\": \"文件已存在\",\n  \"error.fileNotFound\": \"文件未找到\",\n  \"error.fileSelectFailed\": \"文件选择失败\",\n  \"error.genCountRange\": \"生成数量必须在1-10之间\",\n  \"error.loadRecordFailed\": \"加载记录失败 {error}\",\n  \"error.maxSelection\": \"最多只能选择{count}个\",\n  \"error.modelArchMismatch\": \"模型架构不匹配\",\n  \"error.modelDirIdentifyFailed\": \"模型目录识别失败，请选择正确的模型目录\",\n  \"error.modelPathInvalid\": \"模型路径不能包含非英文、空格等特殊字符\",\n  \"error.modelPlatformMismatch\": \"模型平台不匹配\",\n  \"error.modelServiceNotRunning\": \"模型服务未运行\",\n  \"error.modelTypeInvalid\": \"模型类型错误\",\n  \"error.modelUnsigned\": \"由于模型文件未完全签名，请运行以下命令完成签名后运行\",\n  \"error.modelVersionExists\": \"模型相同版本已存在\",\n  \"error.nameDuplicate\": \"名称重复\",\n  \"error.noMicrophone\": \"未检测到录音设备\",\n  \"error.parseFailed\": \"解析返回数据失败\",\n  \"error.platformMismatch\": \"平台不匹配\",\n  \"error.processError\": \"处理出错\",\n  \"error.processTimeout\": \"处理超时\",\n  \"error.recognitionModelNotStarted\": \"语音识别模型未启动\",\n  \"error.recognitionParamInvalid\": \"语音识别参数不正确\",\n  \"error.recordNotFound\": \"未找到记录\",\n  \"error.requestError\": \"请求错误\",\n  \"error.requestFailed\": \"请求失败\",\n  \"error.responseEmpty\": \"返回数据为空\",\n  \"error.resumeFailed\": \"继续失败\",\n  \"error.saveFileFailed\": \"保存文件失败: {error}\",\n  \"error.selectFileFailed\": \"选择文件失败:{error}\",\n  \"error.softwareVersionMismatch\": \"软件不满足模型版本要求\",\n  \"error.soundAsrResultEmpty\": \"SoundAsr 识别结果为空，请检查音频文件是否正常\",\n  \"error.soundGenerateResultEmpty\": \"SoundGenerate 生成结果为空，请检查参数是否正确\",\n  \"error.textToImageResultEmpty\": \"TextToImage 生成结果为空，请检查参数是否正确\",\n  \"error.imageToImageResultEmpty\": \"ImageToImage 生成结果为空，请检查参数是否正确\",\n  \"error.taskFailed\": \"任务失败\",\n  \"error.taskNotFound\": \"任务不存在\",\n  \"error.getTaskFailed\": \"任务获取失败\",\n  \"error.timbreNotFound\": \"声音音色不存在\",\n  \"error.updateFailed\": \"更新失败\",\n  \"error.videoProcessFailed\": \"视频处理失败，请选择其他视频\",\n  \"error.voiceModelNotStarted\": \"声音模型未启动\",\n  \"error.voiceParamInvalid\": \"声音合成参数不正确\",\n  \"feedback.anytime\": \"遇到问题随时反馈\",\n  \"feedback.help\": \"使用遇到问题？发帖求助\",\n  \"feedback.toolRequest\": \"工具需求\",\n  \"fastPanel.shortcuts\": \"快捷动作\",\n  \"form.inputField\": \"请输入{title}\",\n  \"form.required\": \"{title}不能为空\",\n  \"group.name\": \"分组名称\",\n  \"guide.audioReq1\": \"1. 请在安静的环境下进行录音，避免噪音干扰\",\n  \"guide.audioReq2\": \"2. 请使用标准普通话，吐字清晰，语速适当\",\n  \"guide.audioReq3\": \"3. 录音时长控制在 6～20秒 最佳，最多不超过20秒\",\n  \"guide.audioReq4\": \"4. 录制完成后先试听看是否达到要求再提交\",\n  \"guide.videoReq1\": \"1. 视频时长要求在10秒～30秒，视频格式为MP4，建议分辨率1080p~4K\",\n  \"guide.videoReq2\": \"2. 为保障效果，视频必须保证每一帧都要正面露脸，脸部无任何遮挡，并且视频中只能出现同一个人脸\",\n  \"guide.videoReq3\": \"3. 视频人物建议闭口或微微张口，张口幅度不宜过大，距离镜头一定距离，可根据合成效果自行调整\",\n  \"guide.videoReq4\": \"4. 不能全程闭嘴，可以正常语气循环说 一二三四五六七八九 等文字\",\n  \"help.howToAddModel\": \"如何添加模型？\",\n  \"home.welcome\": \"欢迎使用\",\n  \"hotkey.doubleClick\": \"双击\",\n  \"hotkey.instructions\": \"使用方式：\",\n  \"hotkey.notSet\": \"未设置\",\n  \"hotkey.step1\": \"① 点击激活\",\n  \"hotkey.step2Mac\": \"② 先按功能键（Control、Command、Option）再按其他普通键，也可快速按快功能键2次\",\n  \"hotkey.step2Win\": \"② 先按功能键（Ctrl、Shift、Alt）再按其他普通键，也可快速按快功能键2次\",\n  \"hint.addContent\": \"请添加内容\",\n  \"hint.audioFormat\": \"支持 wav/mp3 格式\",\n  \"hint.configPromptFirst\": \"请先配置提示词\",\n  \"hint.fileTypes\": \"支持的文件类型\",\n  \"hint.inputContent\": \"请输入内容\",\n  \"hint.inputKeywords\": \"请输入关键词\",\n  \"hint.inputName\": \"请输入名称\",\n  \"hint.inputPreviewText\": \"输入预览文字\",\n  \"hint.inputRandomText\": \"请输入随机话术\",\n  \"hint.inputRefText\": \"请输入参考文字\",\n  \"hint.inputRequirement\": \"请输入你的需求\",\n  \"hint.inputStandardText\": \"请输入标准话术\",\n  \"hint.inputSynthesisContent\": \"请输入合成内容\",\n  \"hint.inputTagEnter\": \"输入标签后回车\",\n  \"hint.inputVoiceSynthesis\": \"输入语音内容开始合成\",\n  \"hint.recordVoice\": \"请录制声音\",\n  \"hint.selectAudioFile\": \"请选择音频文件\",\n  \"hint.selectAvatar\": \"请选择形象\",\n  \"hint.selectAvatarModel\": \"请选择数字人模型\",\n  \"hint.selectFileFormat\": \"请选择{extensions}格式的文件\",\n  \"hint.selectModel\": \"请选择模型\",\n  \"hint.selectModelCheck\": \"请选择要检测的模型\",\n  \"hint.selectModelFirst\": \"请先选择模型\",\n  \"hint.selectPlatform\": \"请选择模型平台\",\n  \"hint.selectRecognitionModel\": \"请选择语音识别模型\",\n  \"hint.selectSynthesisType\": \"请选择合成类型\",\n  \"hint.selectTimbre\": \"请选择声音音色\",\n  \"hint.selectVideo\": \"请选择视频\",\n  \"hint.selectVideoFile\": \"请选择视频文件\",\n  \"hint.selectVoice\": \"请选择声音\",\n  \"hint.selectVoiceModel\": \"请选择声音模型\",\n  \"intro.interactionSupport\": \"互动交流支持各大平台\",\n  \"intro.lipSync\": \"支持音频驱动实现口型替换\",\n  \"intro.modelsSupported\": \"上千种音色模型支持\",\n  \"intro.modelsUpdate\": \"多种开源模型持续更新\",\n  \"intro.textToVideo\": \"输入文本自动合成音频驱动口型合成视频\",\n  \"intro.voiceClone\": \"支持内置声音合成，5秒音频声音克隆\",\n  \"live.knowledge\": \"直播知识\",\n  \"live.knowledgeUpdateHint\": \"知识库更新，直播数据将会在30秒后更新\",\n  \"live.knowledgeUpdated\": \"直播知识库已更新\",\n  \"live.noAvatarSelected\": \"没有选择播放的数字人\",\n  \"live.noLoopMaterialSelected\": \"没有选择循环素材\",\n  \"live.setLiveRoomAddressFirst\": \"请先设置直播间地址\",\n  \"live.live\": \"直播\",\n  \"log.autoScroll\": \"自动滚动\",\n  \"log.view\": \"日志查看\",\n  \"mcp.noTools\": \"暂无可用工具\",\n  \"mcp.serverAddress\": \"MCP Server 地址\",\n  \"media.audioFile\": \"音频文件\",\n  \"media.cropAudio\": \"裁剪音频\",\n  \"media.cropConfirm\": \"确定裁剪\",\n  \"media.selectAudio\": \"选择音频文件\",\n  \"media.selectVideo\": \"选择视频文件\",\n  \"media.subtitle\": \"字幕\",\n  \"media.subtitlePreview\": \"字幕预览\",\n  \"media.video\": \"视频\",\n  \"monitor.debug\": \"调试\",\n  \"monitor.refresh\": \"刷新\",\n  \"model.accelerationOn\": \"连续调用加速已开启\",\n  \"model.embedModels\": \"嵌入模型\",\n  \"model.freeModels\": \"免费模型\",\n  \"model.builtinModels\": \"大模型\",\n  \"model.add\": \"添加模型\",\n  \"model.addCloud\": \"添加云端模型\",\n  \"model.addLocal\": \"添加本地模型\",\n  \"model.addProvider\": \"添加供应商\",\n  \"model.addSuccess\": \"模型添加成功\",\n  \"model.builtinDesc\": \"内置模型无需配置可直接使用\",\n  \"model.censorResult\": \"违规词检测结果\",\n  \"model.cloudAvatar\": \"云端形象\",\n  \"model.cloudModel\": \"云端模型\",\n  \"model.cloudModelDesc\": \"云端模型支持直接使用，无需下载和安装，方便快捷\",\n  \"model.cloudModelService\": \"云端模型服务\",\n  \"model.cloudVideoAvatar\": \"云端视频形象\",\n  \"model.cloudVideoAvatarDesc\": \"云端视频形象，支持直接下载到本地使用\",\n  \"model.deleteConfirm\": \"确定删除模型 {title} v{version} 吗？\",\n  \"model.description\": \"模型说明\",\n  \"model.download\": \"下载模型\",\n  \"model.edit\": \"编辑模型\",\n  \"model.editProvider\": \"编辑供应商\",\n  \"model.hardwareReq\": \"硬件要求\",\n  \"model.id\": \"模型ID\",\n  \"model.img2img\": \"图生图\",\n  \"model.info\": \"模型信息\",\n  \"model.list\": \"模型列表\",\n  \"model.localModel\": \"本地模型\",\n  \"model.market\": \"模型市场\",\n  \"model.marketTip\": \"访问模型市场，下载模型到本地\",\n  \"model.model\": \"模型\",\n  \"model.name\": \"模型名称\",\n  \"model.notSupported\": \"模型不支持\",\n  \"model.runInCloudDesc\": \"模型运行在云端，避免本地资源不足\",\n  \"model.runInLocalDesc\": \"模型运行在本地，对电脑性能有要求\",\n  \"model.searchPlatform\": \"搜索模型平台\",\n  \"model.seedTip\": \"点击生成随机种子，种子相同则生成的结果相同\",\n  \"model.select\": \"选择模型\",\n  \"model.selectLocal\": \"选择本地模型\",\n  \"model.signature\": \"模型文件签名\",\n  \"model.systemPrompt\": \"系统提示词\",\n  \"model.txt2img\": \"文生图\",\n  \"model.userPrompt\": \"用户提示词\",\n  \"model.unzipTip\": \"解压模型压缩包，选择目录中的config.json文件\",\n  \"model.versionReq\": \"版本要求\",\n  \"msg.copiedToClipboard\": \"已复制 {text} 剪切板\",\n  \"msg.fileSavedTo\": \"文件已保存到 {path}\",\n  \"msg.moreContent\": \"更多内容，请查看\",\n  \"msg.moreTools\": \"更多工具提交需求给我们\",\n  \"msg.passwordRequired\": \"运行过程可能需要输入密码\",\n  \"msg.requestSuccessPlaying\": \"请求成功，开始播放\",\n  \"msg.stopRequested\": \"已发送停止请求，请等待运行停止\",\n  \"msg.videoProcessing\": \"视频处理可能需要较长时间，请耐心等待\",\n  \"nav.apps\": \"应用工具\",\n  \"nav.feedback\": \"工单反馈\",\n  \"nav.guide\": \"新手指引\",\n  \"nav.home\": \"首页\",\n  \"nav.log\": \"日志\",\n  \"nav.toolbox\": \"工具箱\",\n  \"nav.userCenter\": \"用户中心\",\n  \"payment.error\": \"出错了\",\n  \"payment.expired\": \"已过期\",\n  \"payment.paidClosing\": \"已支付，即将关闭\",\n  \"payment.payWithinSeconds\": \"{seconds}秒内支付\",\n  \"payment.qrcodeExpired\": \"二维码已过期\",\n  \"payment.scanQRCode\": \"微信 / 支付宝 扫一扫\",\n  \"payment.scanned\": \"已扫码\",\n  \"placeholder.chatgpt\": \"例如 ChatGPT\",\n  \"placeholder.gpt35\": \"例如 GPT-3.5\",\n  \"placeholder.requiredGpt\": \"必填 如 gpt-3.5-turbo\",\n  \"plugin.autoDetachWindow\": \"自动分离为独立窗口显示\",\n  \"plugin.backendLog\": \"插件后端日志\",\n  \"plugin.debugWindow\": \"插件调试窗口\",\n  \"plugin.disabled\": \"已禁用\",\n  \"plugin.enabled\": \"已启用\",\n  \"plugin.installFailed\": \"安装失败\",\n  \"plugin.installLocalConfig\": \"选择插件config.json\",\n  \"plugin.installLocalZip\": \"选择本地ZIP插件\",\n  \"plugin.installSuccess\": \"安装成功\",\n  \"plugin.localPlugin\": \"本地插件:{path}\",\n  \"plugin.market\": \"插件市场\",\n  \"plugin.notFound\": \"没有找到插件\",\n  \"plugin.publish\": \"发布插件\",\n  \"plugin.publishFailed\": \"发布失败:{error}\",\n  \"plugin.publishSuccess\": \"发布成功\",\n  \"plugin.publishing\": \"正在发布\",\n  \"plugin.refreshSuccess\": \"刷新成功\",\n  \"plugin.search\": \"搜索插件\",\n  \"plugin.uninstall\": \"卸载\",\n  \"plugin.uninstallConfirm\": \"确定要卸载插件吗？\",\n  \"plugin.uninstallFailed\": \"卸载失败:{error}\",\n  \"plugin.uninstallSuccess\": \"卸载成功\",\n  \"plugin.updateInfo\": \"更新信息\",\n  \"plugin.updateInfoFailed\": \"更新资料失败\",\n  \"plugin.updateInfoSuccess\": \"更新资料成功\",\n  \"plugin.updatingInfo\": \"正在更新资料\",\n  \"proUpgrade.defaultDesc\": \"请下载 Pro 版本解锁完整功能\",\n  \"proUpgrade.downloadButton\": \"立即下载 Pro 版本\",\n  \"proUpgrade.title\": \"功能升级提示\",\n  \"provider.baichuan\": \"百川\",\n  \"provider.baiduCloud\": \"百度云千帆\",\n  \"provider.buildIn\": \"大模型\",\n  \"provider.dashscope\": \"阿里云百炼\",\n  \"provider.deepseek\": \"深度求索\",\n  \"provider.doubao\": \"火山引擎\",\n  \"provider.hunyuan\": \"腾讯混元\",\n  \"provider.infini\": \"无问芯穹\",\n  \"provider.modelscope\": \"ModelScope 魔搭\",\n  \"provider.moonshot\": \"月之暗面\",\n  \"provider.nvidia\": \"英伟达\",\n  \"provider.ppio\": \"PPIO 派欧云\",\n  \"provider.silicon\": \"硅基流动\",\n  \"provider.stepfun\": \"阶跃星辰\",\n  \"provider.tencentCloudTi\": \"腾讯云TI\",\n  \"provider.xirang\": \"天翼云息壤\",\n  \"provider.yi\": \"零一万物\",\n  \"provider.zhinao\": \"360智脑\",\n  \"provider.zhipu\": \"智谱AI\",\n  \"service.start\": \"启动服务\",\n  \"service.startFailed\": \"启动失败\",\n  \"service.starting\": \"启动中\",\n  \"setting.altDoubleClick\": \"Alt双击\",\n  \"setting.altSingleClick\": \"Alt单击\",\n  \"setting.apiKey\": \"API密钥\",\n  \"setting.apiUrl\": \"API地址\",\n  \"setting.askEveryTime\": \"每次询问\",\n  \"setting.autoLaunch\": \"开机启动\",\n  \"setting.autoStart\": \"自启动\",\n  \"setting.autoUpdate\": \"自动检测更新\",\n  \"setting.basic\": \"基础设置\",\n  \"setting.commandDoubleClick\": \"Command双击\",\n  \"setting.commandSingleClick\": \"Command单击\",\n  \"setting.controlDoubleClick\": \"Control双击\",\n  \"setting.controlSingleClick\": \"Control单击\",\n  \"setting.ctrlDoubleClick\": \"Ctrl双击\",\n  \"setting.ctrlSingleClick\": \"Ctrl单击\",\n  \"setting.cudaAcceleration\": \"CUDA加速\",\n  \"setting.dataConfig\": \"数据配置\",\n  \"setting.detachWindowHotkey\": \"分离窗口快捷键\",\n  \"setting.env\": \"环境设置\",\n  \"setting.exitDirectly\": \"直接退出\",\n  \"setting.fixCommand\": \"修复命令\",\n  \"setting.followSystem\": \"跟随系统\",\n  \"setting.interfaceType\": \"接口类型\",\n  \"setting.llm\": \"大模型设置\",\n  \"setting.localModelDir\": \"本地模型目录\",\n  \"setting.onClose\": \"点击关闭时\",\n  \"setting.optionDoubleClick\": \"Option双击\",\n  \"setting.optionSingleClick\": \"Option单击\",\n  \"setting.pathChangeConfirm\": \"确认修改存储路径为 {path} ？\",\n  \"setting.pathChangeRestart\": \"修改存储路径需要重启软件\",\n  \"setting.storagePath\": \"文件存储路径\",\n  \"setting.themeStyle\": \"主题样式\",\n  \"setting.triggerType\": \"触发类型\",\n  \"setting.wpm\": \"每分钟字数\",\n  \"setup.allCompleted\": \"已完成所有设置\",\n  \"setup.congratulations\": \"恭喜完成 {title} 设置\",\n  \"setup.openSettings\": \"打开设置\",\n  \"setup.verifyComplete\": \"验证完成\",\n  \"soundAsr.copyResult\": \"复制识别结果\",\n  \"soundAsrEdit.inputSearchContent\": \"请输入查找内容\",\n  \"soundAsrEdit.invalidTimeRange\": \"时间范围不合法，必须在记录中间\",\n  \"soundAsrEdit.mergedBlankSegments\": \"已合并连续空白片段\",\n  \"soundAsrEdit.mergeOnlyContinuous\": \"只能合并连续的记录\",\n  \"soundAsrEdit.noEditRecord\": \"没有编辑记录\",\n  \"soundAsrEdit.noMatchFound\": \"未找到匹配的内容\",\n  \"soundAsrEdit.optimizeComplete\": \"优化完成，成功修复 {successCount} 句，失败 {failCount} 句\",\n  \"soundAsrEdit.replacedRecords\": \"已替换 {count} 条记录\",\n  \"soundAsrEdit.time\": \"时间\",\n  \"soundReplace.confirmComplete\": \"确认无误完成\",\n  \"soundReplace.confirmText\": \"确认文字\",\n  \"soundReplace.confirmTextDesc\": \"检查并确认识别出的文本内容\",\n  \"soundReplace.extractAndRecognize\": \"提取音频并识别\",\n  \"soundReplace.extractAndRecognizeDesc\": \"选择包含需要替换声音的视频文件\",\n  \"soundReplace.extractAudio\": \"提取音频\",\n  \"soundReplace.manualConfirmText\": \"手动确认文字\",\n  \"soundReplace.modifyText\": \"修改文字\",\n  \"soundReplace.reorderConfirm\": \"重排确认\",\n  \"soundReplace.reverifyText\": \"重新校验文字\",\n  \"soundReplace.saveAndSynthesize\": \"保存并合成\",\n  \"soundReplace.submitTask\": \"提交任务\",\n  \"soundReplace.synthesizeReplace\": \"声音合成替换\",\n  \"soundReplace.synthesizeReplaceDesc\": \"设置声音合成模型参数，生成新的语音\",\n  \"soundReplace.taskSubmitted\": \"任务已提交\",\n  \"soundReplace.videoSynthesis\": \"视频合成\",\n  \"status.cancelling\": \"正在取消任务\",\n  \"status.deleting\": \"正在删除\",\n  \"status.downloading\": \"正在下载\",\n  \"status.downloadingProgress\": \"正在下载 {index}/{total}\",\n  \"status.loading\": \"正在加载\",\n  \"status.manuallyCompleted\": \"已手动完成\",\n  \"status.notRunning\": \"暂未运行\",\n  \"status.queuing\": \"排队中\",\n  \"status.resuming\": \"正在继续\",\n  \"status.retrying\": \"正在重试\",\n  \"status.running\": \"运行中\",\n  \"status.startedTime\": \"已启动 {time}\",\n  \"status.stopped\": \"已停止\",\n  \"status.submitting\": \"正在提交\",\n  \"status.unprocessed\": \"未处理\",\n  \"status.waiting\": \"等待中\",\n  \"subtitleTts.audioSynthesis\": \"音频合成\",\n  \"subtitleTts.parseSubtitle\": \"解析字幕\",\n  \"subtitleTts.settings\": \"字幕转音频设置\",\n  \"subtitleTts.synthesizedAudio\": \"合成音频\",\n  \"store.searchPlaceholder\": \"输入关键词搜索\",\n  \"system.actionManagement\": \"动作管理\",\n  \"system.addFileLaunch\": \"增加一个文件启动\",\n  \"system.aiModel\": \"AI模型\",\n  \"system.dataCenter\": \"数据中心\",\n  \"system.fileLaunch\": \"文件启动\",\n  \"system.functionSettings\": \"功能设置\",\n  \"system.hotkeys\": \"快捷键\",\n  \"system.myAccount\": \"我的账号\",\n  \"system.personalCenter\": \"个人中心\",\n  \"system.pluginManagement\": \"插件管理\",\n  \"system.preferences\": \"偏好设置\",\n  \"task.batchTextSynthesis\": \"批量文本合成\",\n  \"task.cancel\": \"取消任务\",\n  \"task.cancelled\": \"任务已取消\",\n  \"task.cloneSubmitted\": \"任务已经提交成功，等待克隆完成\",\n  \"task.details\": \"任务详情\",\n  \"task.editResult\": \"编辑识别结果\",\n  \"task.longTextToAudio\": \"长文本转音频\",\n  \"task.oneClickRun\": \"一键运行\",\n  \"task.optimizeTimeline\": \"一键优化时间线\",\n  \"task.processing\": \"处理中\",\n  \"task.recognitionSubmitted\": \"语音识别任务已提交\",\n  \"task.resume\": \"继续任务\",\n  \"task.retry\": \"重试任务\",\n  \"task.startRecognition\": \"开始识别\",\n  \"task.startSynthesis\": \"开始合成\",\n  \"task.startVideoGen\": \"开始生成视频\",\n  \"task.submitSynthesis\": \"提交合成\",\n  \"task.subtitleToAudio\": \"字幕转音频\",\n  \"task.synthesisType\": \"合成类型\",\n  \"task.synthesize\": \"合成\",\n  \"task.videoGenSubmitted\": \"任务已经提交成功，等待视频生成完成\",\n  \"task.view\": \"查看任务\",\n  \"theme.dark\": \"暗黑\",\n  \"theme.light\": \"明亮\",\n  \"time.hour\": \"小时\",\n  \"time.minute\": \"分钟\",\n  \"time.second\": \"秒\",\n  \"update.alreadyLatest\": \"已经是最新版本\",\n  \"update.check\": \"检测更新\",\n  \"update.checkFailed\": \"检测更新失败\",\n  \"update.newVersionFound\": \"发现新版本{version}，是否立即下载更新？\",\n  \"user.comment\": \"用户评论\",\n  \"user.energy\": \"能量\",\n  \"user.enter\": \"用户进入\",\n  \"user.knowledge\": \"用户知识\",\n  \"user.like\": \"用户点赞\",\n  \"user.name\": \"用户名\",\n  \"user.reward\": \"用户打赏\",\n  \"video.contentVideo\": \"内容视频\",\n  \"video.loopBroadcast\": \"循环口播\",\n  \"video.loopContent\": \"循环内容视频\",\n  \"video.providerName\": \"供应商名称\",\n  \"voice.add\": \"添加音色\",\n  \"voice.clone\": \"声音克隆\",\n  \"voice.cloneModel\": \"声音克隆模型\",\n  \"voice.config\": \"声音配置\",\n  \"voice.crossLanguage\": \"跨语种\",\n  \"voice.currentConfig\": \"当前声音合成配置\",\n  \"voice.file\": \"声音文件\",\n  \"voice.recognition\": \"语音识别\",\n  \"voice.recognitionConfig\": \"语音识别配置\",\n  \"voice.recognitionModel\": \"语音识别模型\",\n  \"voice.record\": \"录制音频\",\n  \"voice.refAudioGuide1\": \"参考声音控制在 6～20s，保证声音清晰可见\",\n  \"voice.refAudioGuide2\": \"参考声音需要大于 6s 小于 20s，保证声音清晰可见\",\n  \"voice.refTextRequired\": \"需要输入参考声音的完整文字内容，部分模型需要使用\",\n  \"voice.referenceAudio\": \"参考声音\",\n  \"voice.referenceText\": \"参考文字\",\n  \"voice.replace\": \"声音替换\",\n  \"voice.replaceConfig\": \"声音替换设置\",\n  \"voice.rerecord\": \"重新录制\",\n  \"voice.select\": \"选择声音\",\n  \"voice.selectFile\": \"选择声音文件\",\n  \"voice.selectTimbre\": \"选择音色\",\n  \"voice.synthesis\": \"语音合成\",\n  \"voice.synthesisConfig\": \"声音合成配置\",\n  \"voice.synthesisModel\": \"声音合成模型\",\n  \"voice.timbre\": \"声音音色\",\n  \"voice.timbreDesc\": \"音色说明\",\n  \"voice.timbreManage\": \"音色管理\",\n  \"voice.voice\": \"声音\",\n  \"welcome.title\": \"欢迎使用 AIGCPanel !\",\n  \"workflow.create\": \"创建工作流\",\n  \"workflow.workflow\": \"工作流\",\n  \"workflow.configureRecognitionAndGeneration\": \"请配置声音识别和声音生成服务\",\n  \"workflow.inputVideoParam\": \"请输入视频参数\",\n  \"workflow.paramErrorMissing\": \"参数错误：缺少 {items}\",\n  \"workflow.soundGenerationService\": \"声音生成服务\",\n  \"workflow.soundRecognitionService\": \"声音识别服务\",\n  \"about.devModeSettings\": \"开发模式设置\",\n  \"about.fastPanelHideOnBlur\": \"快速面板失焦隐藏\",\n  \"action.backendCode\": \"后端代码\",\n  \"action.code\": \"代码\",\n  \"action.command\": \"命令\",\n  \"action.smartArea\": \"智能区域\",\n  \"action.webpage\": \"网页\",\n  \"backup.connectFailed\": \"连接失败\",\n  \"backup.fileFormat\": \"文件格式\",\n  \"backup.notConfigured\": \"未配置WebDav服务，点击配置\",\n  \"backup.password\": \"密码\",\n  \"backup.placeholderSupport\": \"占位符支持\",\n  \"backup.rootDir\": \"根目录\",\n  \"backup.selectFile\": \"选择需要恢复的文件\",\n  \"backup.selectRestoreFile\": \"请选择需要恢复的文件\",\n  \"backup.startBackup\": \"开始备份\",\n  \"backup.startRestore\": \"开始恢复\",\n  \"backup.uploadToCloud\": \"上传到云端\",\n  \"backup.restoreFromCloud\": \"从云端恢复\",\n  \"backup.username\": \"用户名\",\n  \"backup.webdavConfig\": \"配置\",\n  \"backup.webdavSettings\": \"WebDav配置\",\n  \"data.backupRestore\": \"备份/恢复\",\n  \"data.clear\": \"清空\",\n  \"data.clearConfirm\": \"确定要清空吗？\",\n  \"data.clearSuccess\": \"清空成功\",\n  \"data.deleteConfirm\": \"确定要删除吗？\",\n  \"data.deleteSuccess\": \"删除成功\",\n  \"data.docCount\": \"份文档\",\n  \"data.filterPlaceholder\": \"输入关键词过滤\",\n  \"data.title\": \"数据中心\",\n  \"launch.actionName\": \"动作名称，如 截图\",\n  \"launch.addHotkey\": \"增加一个快捷键\",\n  \"launch.custom\": \"自定义\",\n  \"launch.enterActionName\": \"请输入动作名称\",\n  \"launch.hotkey\": \"快捷键\",\n  \"log.noLogs\": \"暂无日志文件\",\n  \"log.openFile\": \"打开文件\",\n  \"main.clearAllConfirm\": \"确认清除全部？\",\n  \"main.expandAll\": \"展开全部\",\n  \"main.loading\": \"正在加载\",\n  \"main.matchResults\": \"匹配结果\",\n  \"main.multipleFiles\": \"多个文件\",\n  \"main.multipleFolders\": \"多个文件夹\",\n  \"main.multipleImages\": \"多个图片\",\n  \"main.pinned\": \"已固定\",\n  \"main.placeholder\": \"FocusAny，让您的工作专注高效\",\n  \"main.recentlyUsed\": \"最近使用\",\n  \"main.runError\": \"运行出现错误\",\n  \"main.searchResults\": \"搜索结果\",\n  \"main.starting\": \"正在启动\",\n  \"main.window\": \"窗口\",\n  \"plugin.detachWindow\": \"独立窗口显示\",\n  \"plugin.actionNotFound\": \"未找到相关操作，请检查关键词或操作名称是否正确\",\n  \"plugin.colorCopied\": \"颜色 {color} 已复制到剪贴板\",\n  \"plugin.colorCopyShortcut\": \"复制 {shortcut}\",\n  \"plugin.errorLog\": \"插件{name}错误 : {error}\",\n  \"plugin.exitEsc\": \"退出 ESC\",\n  \"plugin.installComplete\": \"插件 {title} 安装完成\",\n  \"plugin.installing\": \"正在安装插件\",\n  \"plugin.newWindow\": \"新窗口\",\n  \"plugin.noPermission\": \"插件没有权限({permission})\",\n  \"plugin.opening\": \"正在打开插件\",\n  \"plugin.notExist\": \"插件 {name} 不存在\",\n  \"plugin.screenshotHint\": \"请使用截图工具截图\",\n  \"plugin.selectSavePath\": \"选择保存路径\",\n  \"editor.noPluginForFile\": \"没有找到可以打开文件的插件\",\n  \"file.notFoundOrReadFailed\": \"文件不存在或读取失败\",\n  \"file.unsupportedType\": \"不支持的文件类型\",\n  \"screenshot.edit\": \"截图编辑\",\n  \"system.title\": \"系统设置\",\n  \"system.desc\": \"提供基础系统功能\",\n  \"system.apps\": \"应用软件\",\n  \"system.appsDesc\": \"提供系统应用软件的搜索和打开\",\n  \"system.appsIndexed\": \"应用软件索引完成\",\n  \"system.appsIndexing\": \"正在分析应用软件，稍后才可以搜索到应用软件哦~\",\n  \"system.fileLaunchDesc\": \"提供文件一键启动功能\",\n  \"system.workflowDesc\": \"提供工作流管理功能\",\n  \"system.storeDesc\": \"提供插件应用市场管理功能\",\n  \"system.about\": \"关于我们\",\n  \"system.screenshot\": \"截图\",\n  \"system.colorPicker\": \"颜色拾取\",\n  \"system.screenRecord\": \"屏幕录制\",\n  \"system.lockScreen\": \"锁屏\",\n  \"system.lanIP\": \"局域网IP\",\n  \"system.ipCopied\": \"IP地址 {ip} 已复制到剪贴板\",\n  \"tray.visitWebsite\": \"访问官网\",\n  \"debug.info\": \"调试信息\",\n  \"debug.copyRoute\": \"复制路由\",\n  \"setting.darkTheme\": \"暗黑\",\n  \"setting.fastPanel\": \"快捷面板\",\n  \"setting.fastPanelHotkey\": \"快捷面板呼出快捷键\",\n  \"setting.functionSettings\": \"功能设置\",\n  \"setting.invokeHotkey\": \"呼出快捷键\",\n  \"setting.language\": \"界面语言\",\n  \"setting.lightTheme\": \"明亮\"\n}\n"
  },
  {
    "path": "src/layouts/Main.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeMount, onMounted, ref } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport IconWindowMaximize from \"~icons/mdi/window-maximize\";\nimport IconWindowMinimize from \"~icons/mdi/window-minimize\";\nimport AppQuitConfirm from \"../components/AppQuitConfirm.vue\";\nimport { AppConfig } from \"../config\";\nimport { isDev } from \"../lib/env\";\nimport PageNav from \"./../components/PageNav.vue\";\n\nconst router = useRouter();\nconst appQuitConfirm = ref<InstanceType<typeof AppQuitConfirm> | null>(null);\nconst platformName = ref<\"win\" | \"osx\" | \"linux\" | null>(null);\n\nconst doQuit = async () => {\n    await appQuitConfirm.value?.show();\n};\n\nonBeforeMount(() => {\n    platformName.value = window.$mapi?.app?.platformName() ?? null;\n});\n\nonMounted(() => {\n    // document.body.setAttribute('arco-theme', 'dark')\n});\n\nconst debugVisible = ref(false);\nconst currentRoute = ref(\"\");\nconst doDebugCopyRoute = async () => {\n    currentRoute.value = router.currentRoute.value.fullPath;\n    await window.$mapi.app.setClipboardText(currentRoute.value);\n    window.$mapi.app.toast($t(\"common.copySuccess\"), { status: \"success\" });\n};\nconst doDebugToggle = () => {\n    currentRoute.value = router.currentRoute.value.fullPath;\n    debugVisible.value = !debugVisible.value;\n};\n</script>\n<template>\n    <div class=\"window-container\">\n        <div\n            class=\"window-header flex h-10 items-center border-b border-solid border-gray-200 dark:border-gray-800\"\n        >\n            <div class=\"window-header-title flex-grow flex items-center\">\n                <div class=\"pl-2 py-2\">\n                    <img src=\"/logo.svg\" class=\"w-4 t-4\" />\n                </div>\n                <div class=\"p-2 flex-grow\">\n                    {{ AppConfig.title }}\n                </div>\n            </div>\n            <div class=\"p-1 leading-4\">\n                <div\n                    class=\"inline-block w-6 h-6 leading-6 cursor-pointer hover:text-primary mr-1\"\n                    @click=\"$mapi.app.windowMin()\"\n                >\n                    <IconWindowMinimize class=\"text-sm\" />\n                </div>\n                <div\n                    class=\"inline-block w-6 h-6 leading-6 cursor-pointer hover:text-primary mr-1\"\n                    @click=\"$mapi.app.windowMax()\"\n                >\n                    <IconWindowMaximize class=\"text-sm\" />\n                </div>\n                <div\n                    class=\"inline-block w-6 h-6 leading-6 cursor-pointer hover:text-red-500\"\n                    @click=\"doQuit\"\n                >\n                    <icon-close class=\"text-sm\" />\n                </div>\n            </div>\n        </div>\n        <div class=\"window-body\">\n            <div class=\"page-container flex\">\n                <div\n                    class=\"w-16 flex-shrink-0 h-full text-white\"\n                    style=\"background-color: var(--color-bg-page-nav)\"\n                >\n                    <PageNav />\n                </div>\n                <div class=\"flex-grow overflow-y-auto\">\n                    <router-view></router-view>\n                </div>\n            </div>\n        </div>\n    </div>\n    <AppQuitConfirm ref=\"appQuitConfirm\" />\n    <!-- 调试弹窗 -->\n    <template v-if=\"isDev\">\n        <div class=\"fixed top-10 left-0 z-50\">\n            <a-button\n                shape=\"circle\"\n                size=\"mini\"\n                class=\"opacity-50 hover:opacity-100\"\n                @click=\"doDebugToggle\"\n            >\n                <template #icon><icon-bug /></template>\n            </a-button>\n        </div>\n        <div\n            v-if=\"debugVisible\"\n            class=\"fixed top-14 left-4 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-3 w-80\"\n        >\n            <div class=\"font-bold text-sm mb-2\">{{ $t(\"debug.info\") }}</div>\n            <div class=\"flex items-center gap-2\">\n                <div class=\"text-xs text-gray-500 flex-grow truncate\">\n                    {{ currentRoute }}\n                </div>\n                <a-button size=\"mini\" @click=\"doDebugCopyRoute\">\n                    <template #icon><icon-copy /></template>\n                    {{ $t(\"debug.copyRoute\") }}\n                </a-button>\n            </div>\n        </div>\n    </template>\n</template>\n"
  },
  {
    "path": "src/layouts/Raw.vue",
    "content": "<script setup lang=\"ts\">\nimport { getCurrentInstance, onMounted } from \"vue\";\nimport { Dialog } from \"../lib/dialog\";\nimport { t } from \"../lang\";\n\nconst app = getCurrentInstance();\nconst doQuit = () => {\n    Dialog.confirm(t(\"common.confirmQuit\")).then(() => {\n        window.$mapi.app.quit();\n    });\n};\n\nonMounted(() => {\n    // document.body.setAttribute('arco-theme', 'dark')\n});\n</script>\n<template>\n    <div class=\"window-container\">\n        <div class=\"window-header flex h-10 items-center\">\n            <div class=\"window-header-title flex-grow flex items-center\">\n                &nbsp;\n            </div>\n            <div class=\"p-1 leading-4\">\n                <div\n                    class=\"inline-block w-6 h-6 leading-6 cursor-pointer hover:text-red-500\"\n                    @click=\"doQuit\"\n                >\n                    <icon-close class=\"text-sm\" />\n                </div>\n            </div>\n        </div>\n        <div class=\"window-body\">\n            <router-view></router-view>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/lib/api.ts",
    "content": "import axios, { type AxiosInstance, type AxiosRequestConfig } from \"axios\";\nimport { merge } from \"lodash-es\";\nimport { Dialog } from \"./dialog\";\nimport { AppConfig } from \"../config\";\nimport { user } from \"../store/modules/user\";\n\nfunction createService() {\n    const service = axios.create();\n    service.interceptors.request.use(\n        (config) => config,\n        (error) => Promise.reject(error),\n    );\n    service.interceptors.response.use(\n        (response) => {\n            const apiData = response.data;\n            const responseType = response.request?.responseType;\n            if (responseType === \"blob\" || responseType === \"arraybuffer\")\n                return apiData;\n            const code = apiData.code;\n            // if (code === undefined) {\n            //     ElMessage.error(\"非本系统的接口\")\n            //     return Promise.reject(new Error(\"非本系统的接口\"))\n            // }\n            // switch (code) {\n            //     case 0:\n            //         // 本系统采用 code === 0 来表示没有业务错误\n            //         return apiData\n            //     case 401:\n            //         // Token 过期时\n            //         return logout()\n            //     default:\n            //         // 不是正确的 code\n            //         ElMessage.error(apiData.message || \"Error\")\n            //         return Promise.reject(new Error(\"Error\"))\n            // }\n            return apiData;\n        },\n        (error) => {\n            return Promise.reject(error);\n        },\n    );\n    return service;\n}\n\nfunction createRequest(service: AxiosInstance) {\n    return function <T>(config: AxiosRequestConfig): Promise<T> {\n        const defaultConfig = {\n            headers: {\n                \"User-Agent\": window.$mapi.app.getUserAgent(),\n                \"Api-Token\": user.apiToken ? user.apiToken : undefined,\n                \"Content-Type\": \"application/json\",\n            },\n            timeout: 60 * 1000,\n            baseURL: AppConfig.apiBaseUrl,\n            data: {},\n        };\n        // 将默认配置 defaultConfig 和传入的自定义配置 config 进行合并成为 mergeConfig\n        const mergeConfig = merge(defaultConfig, config);\n        return service(mergeConfig).then((response) => response as T);\n    };\n}\n\nconst service = createService();\n\nexport const request = createRequest(service);\n\nexport const defaultResponseProcessor = (\n    res: ApiResult<any>,\n    success: Function | null = null,\n    error: Function | null = null,\n) => {\n    if (res.code) {\n        if (error) {\n            if (!error(res)) {\n                Dialog.tipError(res.msg);\n            }\n        } else {\n            Dialog.tipError(res.msg);\n        }\n    } else {\n        if (success) {\n            if (success(res)) {\n                if (res.msg) {\n                    Dialog.tipSuccess(res.msg);\n                }\n            }\n        } else {\n            if (res.msg) {\n                Dialog.tipSuccess(res.msg);\n            }\n        }\n    }\n};\n"
  },
  {
    "path": "src/lib/audio.ts",
    "content": "export const AudioUtil = {\n    audioBufferEmpty() {\n        const emptyLength = 1024 * 100;\n        const buffer = new AudioBuffer({\n            length: emptyLength,\n            numberOfChannels: 2,\n            sampleRate: 8000,\n        });\n        for (let channel = 0; channel < 2; channel++) {\n            const data = buffer.getChannelData(channel);\n            for (let i = 0; i < emptyLength; i++) {\n                data[i] = 0;\n            }\n        }\n        return buffer;\n    },\n    audioBufferCut(buffer: AudioBuffer, start: number, end: number) {\n        const numChannels = buffer.numberOfChannels;\n        const sampleRate = buffer.sampleRate;\n        const length = buffer.length;\n        const startOffset = Math.floor(start * sampleRate);\n        const endOffset = Math.floor(end * sampleRate);\n        const targetLength = endOffset - startOffset;\n        const targetBuffer = new AudioBuffer({\n            length: targetLength,\n            numberOfChannels: numChannels,\n            sampleRate: sampleRate,\n        });\n        for (let channel = 0; channel < numChannels; channel++) {\n            const sourceChannel = buffer.getChannelData(channel);\n            const targetChannel = targetBuffer.getChannelData(channel);\n            for (let i = 0; i < targetLength; i++) {\n                targetChannel[i] = sourceChannel[startOffset + i];\n            }\n        }\n        return targetBuffer;\n    },\n    audioBufferConvert(\n        buffer: AudioBuffer,\n        targetSampleRate: number,\n        targetChannelNum: number,\n    ) {\n        targetChannelNum = targetChannelNum || buffer.numberOfChannels;\n        const numChannels = buffer.numberOfChannels;\n        const sampleRate = buffer.sampleRate;\n        const length = buffer.length;\n        const targetLength = Math.floor(\n            (length * targetSampleRate) / sampleRate,\n        );\n        const targetBuffer = new AudioBuffer({\n            length: targetLength,\n            numberOfChannels: targetChannelNum,\n            sampleRate: targetSampleRate,\n        });\n        for (let channel = 0; channel < targetChannelNum; channel++) {\n            const sourceChannel = buffer.getChannelData(channel % numChannels);\n            const targetChannel = targetBuffer.getChannelData(channel);\n            for (let i = 0; i < targetLength; i++) {\n                const sourceIndex = Math.floor(\n                    (i * sampleRate) / targetSampleRate,\n                );\n                targetChannel[i] = sourceChannel[sourceIndex];\n            }\n        }\n        return targetBuffer;\n    },\n    audioBufferToWav(buffer: AudioBuffer) {\n        const numChannels = buffer.numberOfChannels;\n        const sampleRate = buffer.sampleRate;\n        const format = 1;\n        const bitDepth = 16;\n        const bytesPerSample = bitDepth / 8;\n        const blockAlign = numChannels * bytesPerSample;\n        const dataSize = buffer.length * blockAlign;\n        const view = new DataView(new ArrayBuffer(44 + dataSize));\n        view.setUint32(0, 1380533830, false);\n        view.setUint32(4, 44 + dataSize - 8, true);\n        view.setUint32(8, 1463899717, false);\n        view.setUint32(12, 1718449184, false);\n        view.setUint32(16, 16, true);\n        view.setUint16(20, format, true);\n        view.setUint16(22, numChannels, true);\n        view.setUint32(24, sampleRate, true);\n        view.setUint32(28, sampleRate * blockAlign, true);\n        view.setUint16(32, blockAlign, true);\n        view.setUint16(34, bitDepth, true);\n        view.setUint32(36, 1635017060, true);\n        view.setUint32(40, dataSize, true);\n        let offset = 44;\n        for (let i = 0; i < buffer.length; i++) {\n            for (let channel = 0; channel < numChannels; channel++) {\n                const sample = buffer.getChannelData(channel)[i];\n                const intSample = Math.max(-1, Math.min(1, sample));\n                view.setInt16(\n                    offset,\n                    Math.round(\n                        intSample < 0 ? intSample * 0x8000 : intSample * 0x7fff,\n                    ),\n                    true,\n                );\n                offset += 2;\n            }\n        }\n        return new Uint8Array(view.buffer);\n    },\n    audioBufferDuration(buffer: AudioBuffer) {\n        return buffer.duration;\n    },\n    audioBufferToWavBlob(buffer: AudioBuffer) {\n        return new Blob([this.audioBufferToWav(buffer)], { type: \"audio/wav\" });\n    },\n    fileToAudioBuffer(file: File) {\n        return new Promise<AudioBuffer>((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onload = () => {\n                const arrayBuffer = reader.result as ArrayBuffer;\n                const context = new AudioContext();\n                context.decodeAudioData(arrayBuffer, resolve, reject);\n            };\n            reader.readAsArrayBuffer(file);\n        });\n    },\n    parseAudioFile(file: File) {\n        return new Promise<{\n            duration: number;\n            sampleRate: number;\n            numberOfChannels: number;\n        }>((resolve, reject) => {\n            this.fileToAudioBuffer(file)\n                .then((buffer) => {\n                    resolve({\n                        duration: buffer.duration,\n                        sampleRate: buffer.sampleRate,\n                        numberOfChannels: buffer.numberOfChannels,\n                    });\n                })\n                .catch(reject);\n        });\n    },\n};\n"
  },
  {
    "path": "src/lib/components/Prompt.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, watch } from \"vue\";\nimport { Input as AInput } from \"@arco-design/web-vue\";\n\nconst props = defineProps<{\n    value: string;\n    onChange: (value: string) => void;\n}>();\n\nconst value = ref(props.value);\nwatch(\n    () => value.value,\n    (newVal) => {\n        props.onChange(newVal);\n    },\n);\n</script>\n\n<template>\n    <div>\n        <a-input v-model=\"value\" />\n    </div>\n</template>\n"
  },
  {
    "path": "src/lib/dialog.ts",
    "content": "import { Message, MessageReturn, Modal } from \"@arco-design/web-vue\";\nimport Prompt from \"./components/Prompt.vue\";\nimport { h } from \"vue\";\nimport { i18n, t } from \"../lang\";\n\nlet loadingLayers: MessageReturn[] = [];\n\nexport const Dialog = {\n    tipSuccess: (msg: string) => {\n        Message.success(msg);\n    },\n    tipError: (msg: string) => {\n        Message.error(msg);\n    },\n    confirm: (content: string, title: string | null = null): Promise<void> => {\n        title = title || t(\"common.tip\");\n        return new Promise((resolve, reject) => {\n            Modal.confirm({\n                title,\n                content,\n                titleAlign: \"start\",\n                simple: false,\n                width: \"25rem\",\n                modalClass: \"arco-modal-confirm\",\n                okText: t(\"common.confirm\"),\n                cancelText: t(\"common.cancel\"),\n                onOk: () => {\n                    resolve();\n                },\n                onCancel: () => {\n                    // reject();\n                },\n            });\n        });\n    },\n    alertSuccess: (\n        content: string,\n        title: string | null = null,\n    ): Promise<void> => {\n        title = title || t(\"common.tip\");\n        return new Promise((resolve) => {\n            Modal.confirm({\n                title,\n                content,\n                simple: false,\n                width: \"25rem\",\n                onOk: () => {\n                    resolve();\n                },\n            });\n        });\n    },\n    alertError: (\n        content: string,\n        title: string | null = null,\n    ): Promise<void> => {\n        title = title || t(\"common.tip\");\n        return new Promise((resolve) => {\n            Modal.confirm({\n                title,\n                content,\n                simple: false,\n                width: \"25rem\",\n                onOk: () => {\n                    resolve();\n                },\n            });\n        });\n    },\n    loadingOn: (content: string | null = null) => {\n        content = content || t(\"common.loadingDots\");\n        const loading = Message.loading({\n            content,\n            duration: 0,\n        });\n        loadingLayers.push(loading);\n    },\n    loadingUpdate: (content: string) => {\n        if (loadingLayers.length > 0) {\n            const contentContainer = document.querySelector(\n                \".arco-message-list .arco-message-loading .arco-message-content\",\n            );\n            if (contentContainer) {\n                contentContainer.innerHTML = content;\n            }\n        }\n    },\n    loadingOff: () => {\n        const loading = loadingLayers.pop();\n        if (loading) {\n            loading.close();\n        }\n    },\n    prompt: (\n        content: string,\n        defaultValue: string = \"\",\n    ): Promise<string | null> => {\n        return new Promise((resolve) => {\n            let inputValue = defaultValue;\n            Modal.open({\n                title: content,\n                simple: false,\n                titleAlign: \"start\",\n                content: () => {\n                    return h(Prompt, {\n                        value: defaultValue,\n                        onChange: (value: string) => {\n                            inputValue = value;\n                        },\n                    });\n                },\n                width: \"25rem\",\n                onOk: () => {\n                    resolve(inputValue);\n                },\n            });\n        });\n    },\n};\n"
  },
  {
    "path": "src/lib/env.ts",
    "content": "export const isDev = process.env.NODE_ENV === \"development\";\n"
  },
  {
    "path": "src/lib/error.ts",
    "content": "import { t } from \"../lang\";\n\nexport function mapError(msg: any) {\n    if (typeof msg !== \"string\") {\n        msg = msg.toString();\n    }\n    const map = {\n        PublishVersionNotMatch: \"error.publishVersionNotMatch\",\n        PluginNotExists: \"error.pluginNotExists\",\n        PluginFormatError: \"error.pluginFormatError\",\n        PluginAlreadyExists: \"error.pluginAlreadyExists\",\n        PluginNotSupportPlatform: \"error.pluginNotSupportPlatform\",\n        PluginVersionNotMatch: \"error.pluginVersionNotMatch\",\n        PluginEditionNotMatch: \"error.pluginEditionNotMatch\",\n        PluginReleaseDocNotFound: \"error.pluginReleaseDocNotFound\",\n        PluginReleaseDocFormatError: \"error.pluginReleaseDocFormatError\",\n    };\n    for (let key in map) {\n        if (msg.includes(key)) {\n            const translationKey = map[key];\n            // regex PluginReleaseDocFormatError:-11\n            const regex = new RegExp(`${key}:(-?\\\\d+):?([\\\\w\\\\d]*)`);\n            const match = msg.match(regex);\n            // console.log('match', match)\n            let error = t(translationKey);\n            if (match) {\n                error += `(${match[1]})`;\n                if (match[2]) {\n                    error += `(${match[2]})`;\n                }\n            }\n            return error;\n        }\n    }\n    return msg;\n}\n"
  },
  {
    "path": "src/lib/event.ts",
    "content": "import { TinyEmitter } from \"tiny-emitter\";\n\nconst emitter = new TinyEmitter();\n\nexport const GlobalEvent = {\n    on: function (event: string, callback: Function) {\n        emitter.on(event, callback);\n    },\n    once: function (event: string, callback: Function) {\n        emitter.once(event, callback);\n    },\n    off: function (event: string, callback: Function) {\n        emitter.off(event, callback);\n    },\n    emit: function (event: string, ...args: any[]) {\n        emitter.emit(event, ...args);\n    },\n};\n"
  },
  {
    "path": "src/lib/file.ts",
    "content": "import SparkMD5 from \"spark-md5\";\n\nexport const FileUtil = {\n    extensionToType(extension: string) {\n        const mime = {\n            mp3: \"audio/mpeg\",\n            wav: \"audio/wav\",\n            mp4: \"video/mp4\",\n            jpg: \"image/jpeg\",\n            jpeg: \"image/jpeg\",\n            png: \"image/png\",\n            gif: \"image/gif\",\n            svg: \"image/svg+xml\",\n        };\n        return mime[extension] || \"\";\n    },\n    bufferToBlob(buffer: ArrayBuffer, type: string) {\n        if (!type.indexOf(\"/\")) {\n            type = this.extensionToType(type);\n        }\n        return new Blob([buffer], { type: type });\n    },\n    base64ToBuffer(base64: string) {\n        const binaryString = window.atob(base64);\n        const len = binaryString.length;\n        const bytes = new Uint8Array(len);\n        for (let i = 0; i < len; i++) {\n            bytes[i] = binaryString.charCodeAt(i);\n        }\n        return bytes.buffer;\n    },\n    blobToFile(blob: Blob, name: string) {\n        return new File([blob], name);\n    },\n    urlToBlob(url: string): Promise<Blob> {\n        return fetch(url).then((res) => res.blob());\n    },\n    blobToBase64Url(blob: Blob): Promise<string> {\n        return new Promise((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onloadend = () => {\n                resolve(reader.result as string);\n            };\n            reader.onerror = (e) => {\n                reject(e);\n            };\n            reader.readAsDataURL(blob);\n        });\n    },\n    getExt(path: string) {\n        const ext = path.lastIndexOf(\".\");\n        if (ext >= 0) {\n            return path.substring(ext + 1).toLowerCase();\n        }\n        return \"\";\n    },\n    getBaseName(path: string, withExt: boolean = false) {\n        // windows\n        if (path.includes(\"\\\\\")) {\n            path = path.replace(/\\\\/g, \"/\");\n        }\n        const last = path.lastIndexOf(\"/\");\n        if (last >= 0) {\n            path = path.substring(last + 1);\n        }\n        if (!withExt) {\n            const ext = path.lastIndexOf(\".\");\n            if (ext >= 0) {\n                path = path.substring(0, ext);\n            }\n            return path;\n        }\n        return path;\n    },\n    async md5File(file: File): Promise<string> {\n        return new Promise((resolve, reject) => {\n            if (!SparkMD5) {\n                reject(new Error(\"SparkMD5 not found\"));\n                return;\n            }\n            const chunkSize = 2097152; // Read in chunks of 2MB\n            const chunks = Math.ceil(file.size / chunkSize);\n            let currentChunk = 0;\n            const spark = new SparkMD5.ArrayBuffer();\n            const fileReader = new FileReader();\n\n            fileReader.onload = (e: any) => {\n                if (e.target.error) {\n                    reject(e.target.error);\n                    return;\n                }\n                spark.append(e.target.result); // Append array buffer\n                currentChunk++;\n                if (currentChunk < chunks) {\n                    loadNext();\n                } else {\n                    const md5 = spark.end();\n                    resolve(md5);\n                }\n            };\n\n            fileReader.onerror = () => {\n                reject(fileReader.error);\n            };\n\n            function loadNext() {\n                const start = currentChunk * chunkSize;\n                const end =\n                    start + chunkSize >= file.size\n                        ? file.size\n                        : start + chunkSize;\n                fileReader.readAsArrayBuffer(file.slice(start, end));\n            }\n\n            loadNext();\n        });\n    },\n    async md5Stream(stream: ReadableStream<Uint8Array>): Promise<string> {\n        if (!SparkMD5) {\n            throw new Error(\"SparkMD5 not found\");\n        }\n        const reader = stream.getReader();\n        const spark: any = new SparkMD5.ArrayBuffer();\n\n        return new Promise((resolve, reject) => {\n            function processChunk() {\n                reader\n                    .read()\n                    .then(({ done, value }) => {\n                        if (done) {\n                            const md5 = spark.end();\n                            resolve(md5);\n                            return;\n                        }\n                        if (value) {\n                            spark.append(value.buffer);\n                        }\n                        processChunk();\n                    })\n                    .catch((err) => {\n                        reject(err);\n                    });\n            }\n\n            processChunk();\n        });\n    },\n    formatSize: (bytes: number) => {\n        if (bytes === 0) return \"0 Bytes\";\n        const k = 1024;\n        const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n        const i = Math.floor(Math.log(bytes) / Math.log(k));\n        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n    },\n};\n"
  },
  {
    "path": "src/lib/markdown.ts",
    "content": "import Showdown from \"showdown\";\n\nconst converter = new Showdown.Converter();\n\nexport const MarkdownUtil = {\n    toHtml(markdown: string): string {\n        return converter.makeHtml(markdown);\n    },\n};\n"
  },
  {
    "path": "src/lib/storage.ts",
    "content": "export const StorageUtil = {\n    /**\n     * @Util 删除\n     * @param key String 键\n     */\n    remove: function (key: string): void {\n        window.localStorage.removeItem(key);\n    },\n    /**\n     * @Util 存储数据\n     * @param key String 键\n     * @param value String|Object|Array 值\n     */\n    set: function (key: string, value: any): void {\n        window.localStorage.setItem(key, JSON.stringify(value));\n    },\n    /**\n     * @Util 获取数据\n     * @param key String 键\n     * @param defaultValue String|Object|Array 默认值\n     * @return String|Object|Array 返回值\n     */\n    get: function (key: string, defaultValue: any): any {\n        let value = window.localStorage.getItem(key);\n        if (null === value) {\n            return defaultValue;\n        }\n        try {\n            return JSON.parse(value);\n        } catch (e) {}\n        return defaultValue;\n    },\n    /**\n     * @Util 获取数组数据\n     * @param key String 键\n     * @param defaultValue Array 默认值\n     * @return Array 返回值\n     */\n    getArray: function (key: string, defaultValue?: any): any {\n        defaultValue = defaultValue || [];\n        let value = window.localStorage.getItem(key);\n        if (!value) {\n            return defaultValue;\n        }\n        try {\n            value = JSON.parse(value);\n            if (!Array.isArray(value)) {\n                return defaultValue;\n            }\n            return value;\n        } catch (e) {}\n        return defaultValue;\n    },\n    /**\n     * @Util 获取对象数据\n     * @param key String 键\n     * @param defaultValue Object 默认值\n     * @return Array 返回值\n     */\n    getObject: function (key: string, defaultValue?: any): any {\n        defaultValue = defaultValue || {};\n        let value = window.localStorage.getItem(key);\n        if (!value) {\n            return defaultValue;\n        }\n        try {\n            value = JSON.parse(value);\n            if (null === value) {\n                return defaultValue;\n            }\n            if (!Array.isArray(value) && typeof value === \"object\") {\n                return value;\n            }\n            return defaultValue;\n        } catch (e) {}\n        return defaultValue;\n    },\n};\n"
  },
  {
    "path": "src/lib/toggle.ts",
    "content": "import { computed, ref, type Ref, type ComputedRef } from \"vue\";\n\nexport const ToggleUtil = {\n    cachePool: new Map<string, { expire: number; value: Ref<boolean> }>(),\n    gc() {\n        const now = Date.now();\n        for (const [key, { expire }] of this.cachePool) {\n            if (expire < now) {\n                this.cachePool.delete(key);\n            }\n        }\n    },\n    get(biz: string, bizId: any, defaultValue: boolean = false) {\n        const key = `Toggle:${biz}:${bizId}`;\n        if (!this.cachePool.has(key)) {\n            const refValue = ref(defaultValue);\n            this.cachePool.set(key, {\n                expire: Date.now() + 3600 * 1000,\n                value: refValue,\n            });\n            return refValue;\n        }\n        const cached = this.cachePool.get(key)!;\n        cached.expire = Date.now() + 3600 * 1000;\n        ToggleUtil.gc();\n        return cached.value;\n    },\n\n    toggle(biz: string, bizId: any) {\n        const refValue = this.get(biz, bizId);\n        refValue.value = !refValue.value;\n        return refValue.value;\n    },\n};\n"
  },
  {
    "path": "src/lib/ui.ts",
    "content": "type DomListener = {\n    dom: HTMLElement;\n    callback: (width: number, height: number) => void;\n};\nlet domListeners: DomListener[] = [];\nconst resizeObserver = new ResizeObserver((entries) => {\n    entries.forEach((entry) => {\n        domListeners.forEach((item) => {\n            if (item.dom === entry.target) {\n                const { width, height } = entry.contentRect;\n                item.callback(width, height);\n            }\n        });\n    });\n});\n\ntype WindowListener = {\n    callback: (width: number, height: number) => void;\n};\nlet windowListeners: WindowListener[] = [];\nwindow.addEventListener(\"resize\", () => {\n    windowListeners.forEach((item) => {\n        item.callback(window.innerWidth, window.innerHeight);\n    });\n});\n\nexport const UI = {\n    onWindowResize(callback: (width: number, height: number) => void) {\n        windowListeners.push({ callback });\n    },\n    offWindowResize(callback: (width: number, height: number) => void) {\n        windowListeners = windowListeners.filter(\n            (item) => item.callback !== callback,\n        );\n    },\n    onResize(\n        dom: HTMLElement | null,\n        callback: (width: number, height: number) => void,\n    ) {\n        if (!dom) return;\n        domListeners.push({ dom, callback });\n        resizeObserver.observe(dom);\n    },\n    offResize(dom: HTMLElement | null) {\n        if (!dom) return;\n        domListeners = domListeners.filter((item) => item.dom !== dom);\n        resizeObserver.unobserve(dom);\n    },\n    fireResize(dom: HTMLElement) {\n        domListeners.forEach((item) => {\n            if (item.dom === dom) {\n                const { width, height } = dom.getBoundingClientRect();\n                item.callback(width, height);\n            }\n        });\n    },\n    smoothScrollTop: (element: HTMLElement, to: number, duration = 200) => {\n        return new Promise((resolve) => {\n            const start = element.scrollTop;\n            const change = to - start;\n            const startTime = performance.now();\n            const animate = (now) => {\n                const progress = Math.min((now - startTime) / duration, 1);\n                const eased =\n                    progress < 0.5\n                        ? 4 * progress * progress * progress\n                        : 1 - Math.pow(-2 * progress + 2, 3) / 2;\n                element.scrollTop = start + change * eased;\n                if (progress < 1) {\n                    requestAnimationFrame(animate);\n                } else {\n                    resolve(undefined);\n                }\n            };\n            requestAnimationFrame(animate);\n        });\n    },\n};\n\nexport class TabContentScroller {\n    private option: {\n        activeClass: string;\n    };\n    private tabContainer: HTMLElement;\n    private contentContainer: HTMLElement;\n    private isScrolling = false;\n    private scrollEndTimer: any | null = null;\n    private scrollEndCallback: (() => void) | null = null;\n\n    constructor(\n        tabContainer: HTMLElement,\n        contentContainer: HTMLElement,\n        option: {} = {},\n    ) {\n        this.option =\n            Object.assign(\n                {\n                    activeClass: \"active\",\n                },\n                option,\n            ) || {};\n        this.tabContainer = tabContainer;\n        this.contentContainer = contentContainer;\n        this.init();\n    }\n\n    init() {\n        this.tabContainer.addEventListener(\n            \"click\",\n            this.onTabClickEvent.bind(this),\n        );\n        this.contentContainer.addEventListener(\n            \"scroll\",\n            this.onContentScrollEvent.bind(this),\n        );\n    }\n\n    destroy() {\n        this.tabContainer.removeEventListener(\n            \"click\",\n            this.onTabClickEvent.bind(this),\n        );\n        this.contentContainer.removeEventListener(\n            \"scroll\",\n            this.onContentScrollEvent.bind(this),\n        );\n    }\n\n    onTabClickEvent(e: MouseEvent) {\n        const parentSection = (e.target as HTMLElement).closest(\n            \"[data-section]\",\n        );\n        const name = parentSection?.getAttribute(\"data-section\");\n        if (name) {\n            this.scrollTo(name);\n            this.scrollEndCallback = () => {\n                this.forceActiveTab(name);\n            };\n        }\n    }\n\n    onContentScrollEvent(e: Event) {\n        this.isScrolling = true;\n        if (this.scrollEndTimer) {\n            clearTimeout(this.scrollEndTimer);\n        }\n        this.scrollEndTimer = setTimeout(() => {\n            this.isScrolling = false;\n            this.scrollEndTimer = null;\n            if (this.scrollEndCallback) {\n                this.scrollEndCallback();\n                this.scrollEndCallback = null;\n            }\n        }, 100);\n        const tabs = this.tabContainer.querySelectorAll(\"[data-section]\");\n        for (let i = 0; i < tabs.length; i++) {\n            const tab = tabs[i];\n            tab.classList.remove(this.option.activeClass);\n        }\n        const sections =\n            this.contentContainer.querySelectorAll(\"[data-section]\");\n        for (let i = 0; i < sections.length; i++) {\n            const section = sections[i];\n            const rect = section.getBoundingClientRect();\n            if (rect.top < 100 && rect.bottom > 100) {\n                const name = section.getAttribute(\"data-section\") || \"\";\n                const tab = this.tabContainer.querySelector(\n                    `[data-section=\"${name}\"]`,\n                );\n                if (tab) {\n                    tab.classList.add(this.option.activeClass);\n                }\n                break;\n            }\n        }\n    }\n\n    forceActiveTab(name: string) {\n        const tabs = this.tabContainer.querySelectorAll(\"[data-section]\");\n        for (let i = 0; i < tabs.length; i++) {\n            const tab = tabs[i];\n            const tabName = tab.getAttribute(\"data-section\") || \"\";\n            if (tabName === name) {\n                tab.classList.add(this.option.activeClass);\n            } else {\n                tab.classList.remove(this.option.activeClass);\n            }\n        }\n    }\n\n    scrollTo(name: string) {\n        const tab = this.tabContainer.querySelector(`[data-section=\"${name}\"]`);\n        if (!tab) {\n            return;\n        }\n        const content = this.contentContainer.querySelector(\n            `[data-section=\"${name}\"]`,\n        );\n        if (!content) {\n            return;\n        }\n        content.scrollIntoView({\n            behavior: \"smooth\",\n        });\n    }\n}\n"
  },
  {
    "path": "src/lib/util.ts",
    "content": "import dayjs from \"dayjs\";\nimport { Base64 } from \"js-base64\";\nimport { t } from \"../lang\";\n\nexport const sleep = (time = 1000) => {\n    return new Promise((resolve) => {\n        setTimeout(() => resolve(true), time);\n    });\n};\n\nexport const wait = (callback: () => boolean, interval = 10) => {\n    return new Promise((resolve) => {\n        const timer = setInterval(() => {\n            if (callback()) {\n                clearInterval(timer);\n                resolve(true);\n            }\n        }, interval);\n    });\n};\n\n/**\n * 精确计时器\n * @param callback\n * @param interval\n * @returns\n */\nexport function preciseInterval(callback: () => void, interval: number) {\n    let expected = performance.now() + interval;\n    let stop = false;\n\n    function step(timestamp: number) {\n        if (stop) return;\n        if (timestamp >= expected) {\n            callback();\n            // 累积期望的时间，以保持精确的间隔\n            expected += interval;\n        }\n        requestAnimationFrame(step);\n    }\n\n    requestAnimationFrame(step);\n    // 返回一个对象包含取消方法\n    return {\n        cancel: () => {\n            stop = true;\n        },\n    };\n}\n\nexport const StringUtil = {\n    random(length: number = 16) {\n        const chars =\n            \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n        let result = \"\";\n        for (let i = 0; i < length; i++) {\n            result += chars.charAt(Math.floor(Math.random() * chars.length));\n        }\n        return result;\n    },\n    uuid: () => {\n        return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(\n            /[xy]/g,\n            function (c) {\n                const r = (Math.random() * 16) | 0;\n                const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n                return v.toString(16);\n            },\n        );\n    },\n    replaceParam: (str: string, param: any) => {\n        return str.replace(/{(.*?)}/g, (match: string, key: string) => {\n            return param[key] || match;\n        });\n    },\n};\n\nexport const TimeUtil = {\n    timestamp() {\n        return Math.floor(Date.now() / 1000);\n    },\n    datetimeToTimestamp(datetime: string) {\n        return dayjs(datetime).unix();\n    },\n    timestampMS() {\n        return Date.now();\n    },\n    format(time: number, format: string = \"YYYY-MM-DD HH:mm:ss\") {\n        return dayjs(time).format(format);\n    },\n    formatDate(time: number) {\n        return dayjs(time).format(\"YYYY-MM-DD\");\n    },\n    dateString() {\n        return dayjs().format(\"YYYYMMDD\");\n    },\n    datetimeString() {\n        return dayjs().format(\"YYYYMMDD_HHmmss\");\n    },\n    secondsToTime(seconds: number, showMs: boolean = false) {\n        const sec = Math.floor(seconds);\n        const ms = Math.floor((seconds - sec) * 1000);\n        let h: any = Math.floor(sec / 3600);\n        let m: any = Math.floor((sec % 3600) / 60);\n        let s: any = Math.floor(sec % 60);\n        if (h < 10) h = \"0\" + h;\n        if (m < 10) m = \"0\" + m;\n        if (s < 10) s = \"0\" + s;\n        const result = \"00\" == h ? `${m}:${s}` : `${h}:${m}:${s}`;\n        if (showMs) {\n            let f: any = ms;\n            if (f < 10) f = \"00\" + f;\n            else if (f < 100) f = \"0\" + f;\n            return `${result}.${f}`;\n        }\n        return result;\n    },\n    msToTime(ms: number) {\n        return this.secondsToTime(ms / 1000, true);\n    },\n    secondsToHuman(seconds: number) {\n        seconds = parseInt(seconds.toString());\n        let h: any = Math.floor(seconds / 3600);\n        let m: any = Math.floor((seconds % 3600) / 60);\n        let s: any = Math.floor(seconds % 60);\n        const result: string[] = [];\n        if (h > 0) result.push(`${h}${t(\"time.hour\")}`);\n        if (m > 0) result.push(`${m}${t(\"time.minute\")}`);\n        if (s > 0) result.push(`${s}${t(\"time.second\")}`);\n        return result.join(\"\");\n    },\n    replacePattern(text: string) {\n        return text\n            .replaceAll(\"{year}\", dayjs().format(\"YYYY\"))\n            .replaceAll(\"{month}\", dayjs().format(\"MM\"))\n            .replaceAll(\"{day}\", dayjs().format(\"DD\"))\n            .replaceAll(\"{hour}\", dayjs().format(\"HH\"))\n            .replaceAll(\"{minute}\", dayjs().format(\"mm\"))\n            .replaceAll(\"{second}\", dayjs().format(\"ss\"));\n    },\n};\n\nexport const EncodeUtil = {\n    base64Encode(str: string) {\n        return Base64.encode(str);\n    },\n    base64Decode(str: string) {\n        return Base64.decode(str);\n    },\n};\n\nexport const VersionUtil = {\n    /**\n     * 检测版本是否匹配\n     * @param v string\n     * @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0\n     */\n    match(v: string, match: string) {\n        if (match === \"*\") {\n            return true;\n        }\n        if (match.startsWith(\">=\") && this.ge(v, match.substring(2))) {\n            return true;\n        }\n        if (match.startsWith(\">\") && this.gt(v, match.substring(1))) {\n            return true;\n        }\n        if (match.startsWith(\"<=\") && this.le(v, match.substring(2))) {\n            return true;\n        }\n        if (match.startsWith(\"<\") && this.lt(v, match.substring(1))) {\n            return true;\n        }\n        return this.eq(v, match);\n    },\n    compare(v1: string, v2: string) {\n        const v1Arr = v1.split(\".\");\n        const v2Arr = v2.split(\".\");\n        for (let i = 0; i < v1Arr.length; i++) {\n            const v1Num = parseInt(v1Arr[i]);\n            const v2Num = parseInt(v2Arr[i]);\n            if (v1Num > v2Num) {\n                return 1;\n            } else if (v1Num < v2Num) {\n                return -1;\n            }\n        }\n        return 0;\n    },\n    gt(v1: string, v2: string) {\n        return VersionUtil.compare(v1, v2) > 0;\n    },\n    ge(v1: string, v2: string) {\n        return VersionUtil.compare(v1, v2) >= 0;\n    },\n    lt(v1: string, v2: string) {\n        return VersionUtil.compare(v1, v2) < 0;\n    },\n    le: (v1: string, v2: string) => {\n        return VersionUtil.compare(v1, v2) <= 0;\n    },\n    eq: (v1: string, v2: string) => {\n        return VersionUtil.compare(v1, v2) === 0;\n    },\n};\n\nexport const BrowserUtil = {\n    isMac() {\n        return navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n    },\n    isWindows() {\n        return navigator.platform.toUpperCase().indexOf(\"WIN\") >= 0;\n    },\n    isLinux() {\n        return navigator.platform.toUpperCase().indexOf(\"LINUX\") >= 0;\n    },\n};\n\nexport const ShellUtil = {\n    quotaPath(p: string) {\n        return `\"${p}\"`;\n    },\n};\n\nexport const ObjectUtil = {\n    clone(obj: any) {\n        return JSON.parse(JSON.stringify(obj));\n    },\n};\n\nexport const DownloadUtil = {\n    downloadFile(content: string, filename?: string) {\n        const blob = new Blob([content], { type: \"application/octet-stream\" });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = filename || `download_${TimeUtil.datetimeString()}.txt`;\n        document.body.appendChild(a);\n        a.click();\n        document.body.removeChild(a);\n        URL.revokeObjectURL(url);\n    },\n};\n"
  },
  {
    "path": "src/main.ts",
    "content": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\nimport router from \"./router\";\nimport store from \"./store\";\n\nimport ArcoVue, { Message } from \"@arco-design/web-vue\";\nimport ArcoVueIcon from \"@arco-design/web-vue/es/icon\";\nimport \"@arco-design/web-vue/dist/arco.css\";\n\nimport { i18n, t } from \"./lang\";\n\nimport \"./style.less\";\nimport { Dialog } from \"./lib/dialog\";\n\nimport { TaskManager } from \"./task\";\nimport { useSettingStore } from \"./store/modules/setting\";\nimport { reportErrorRender } from \"../electron/mapi/log/beacon-render\";\n\nconst settingStore = useSettingStore();\n\nconst app = createApp(App);\napp.use(ArcoVue);\napp.use(ArcoVueIcon);\napp.use(i18n);\napp.use(store);\napp.use(router);\nMessage._context = app._context;\napp.config.globalProperties.$mapi = window.$mapi;\napp.config.globalProperties.$dialog = Dialog;\napp.config.globalProperties.$t = t as any;\nTaskManager.init();\n\napp.mount(\"#app\").$nextTick(() => {\n    postMessage({ payload: \"removeLoading\" }, \"*\");\n\n    window.addEventListener(\"error\", (ev) => {\n        reportErrorRender(\n            ev.message,\n            ev.error?.stack,\n            ev.filename,\n            ev.lineno,\n            ev.colno,\n        );\n    });\n\n    window.addEventListener(\"unhandledrejection\", (ev) => {\n        const err = ev.reason;\n        const msg = err instanceof Error ? err.message : String(err);\n        const stack = err instanceof Error ? err.stack : undefined;\n        reportErrorRender(msg, stack);\n    });\n});\n"
  },
  {
    "path": "src/module/Model/ModelGenerateButton.vue",
    "content": "<script setup lang=\"ts\">\nimport ModelGenerator from \"./ModelGenerator.vue\";\nimport { ref } from \"vue\";\nimport { t } from \"../../lang\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { getDataContent } from \"../../components/common/dataConfig\";\n\nexport type ModelGenerateButtonOptionType = {\n    mode: \"once\" | \"repeat\";\n    promptDefault: string;\n    promptSystem: string;\n    onResult: (result: string, param: Record<string, any>) => Promise<void>;\n    onStart?: () => Promise<void>;\n    onEnd?: () => Promise<void>;\n    onGetParam?: () => Promise<Record<string, any> | null>;\n};\n\nconst props = withDefaults(\n    defineProps<{\n        biz: string;\n        title: string;\n        option: ModelGenerateButtonOptionType;\n    }>(),\n    {\n        title: t(\"common.aiGenerated\"),\n        mode: \"once\",\n    },\n);\nconst modelGenerator = ref<InstanceType<typeof ModelGenerator> | null>(null);\nconst replyGenerateLoading = ref(false);\nconst doGenerateReply = async () => {\n    if (!modelGenerator.value) {\n        Dialog.tipError(t(\"hint.selectModelFirst\"));\n        return;\n    }\n    replyGenerateLoading.value = true;\n    let prompt = await getDataContent(props.biz, props.option.promptDefault);\n    if (!prompt) {\n        Dialog.tipError(t(\"hint.configPromptFirst\"));\n        replyGenerateLoading.value = false;\n        return;\n    }\n    if (props.option.mode === \"repeat\" && !props.option.onGetParam) {\n        Dialog.tipError(\"onGetParam missing\");\n        return;\n    }\n    const chatParam = {\n        systemPrompt: props.option.promptSystem,\n    };\n    try {\n        if (props.option.onStart) {\n            await props.option.onStart();\n        }\n        if (\"once\" === props.option.mode) {\n            let param = {};\n            if (props.option.onGetParam) {\n                param = (await props.option.onGetParam()) || {};\n            }\n            const ret = await modelGenerator.value.chat(\n                prompt,\n                chatParam,\n                param,\n            );\n            if (ret.code) {\n                Dialog.tipError(ret.msg);\n                return;\n            }\n            await props.option.onResult(ret.data?.content!, param);\n        } else if (\"repeat\" === props.option.mode) {\n            for (let i = 0; i < 10000; i++) {\n                let param = await props.option.onGetParam!();\n                if (null === param) {\n                    break;\n                }\n                const ret = await modelGenerator.value.chat(\n                    prompt,\n                    chatParam,\n                    param,\n                );\n                if (ret.code) {\n                    Dialog.tipError(ret.msg);\n                    return;\n                }\n                await props.option.onResult(ret.data?.content!, param);\n            }\n        }\n    } catch (e) {\n        console.error(e);\n        Dialog.tipError(t(\"common.generateFailed\") + \":\" + e);\n    } finally {\n        if (props.option.onEnd) {\n            await props.option.onEnd();\n        }\n        replyGenerateLoading.value = false;\n    }\n};\n</script>\n\n<template>\n    <ModelGenerator ref=\"modelGenerator\" :biz=\"biz\" />\n    <a-button\n        size=\"small\"\n        class=\"mr-1\"\n        @click=\"doGenerateReply\"\n        :loading=\"replyGenerateLoading\"\n    >\n        <template #icon>\n            <icon-robot />\n        </template>\n        {{ title }}\n    </a-button>\n</template>\n"
  },
  {
    "path": "src/module/Model/ModelGenerator.vue",
    "content": "<script setup lang=\"ts\">\nimport ModelSelector from \"./ModelSelector.vue\";\nimport { onMounted, ref, watch } from \"vue\";\nimport { StorageUtil } from \"../../lib/storage\";\nimport { useModelStore } from \"./store/model\";\nimport { StringUtil } from \"../../lib/util\";\nimport { ModelChatResult } from \"./provider/provider\";\nimport { t } from \"../../lang\";\nimport { ChatParam } from \"./types\";\n\nconst modelStore = useModelStore();\nconst selectedModel = ref<string>(\"\");\nconst props = defineProps({\n    biz: {\n        type: String,\n        default: \"\",\n    },\n});\nwatch(selectedModel, (newValue) => {\n    if (props.biz) {\n        StorageUtil.set(`ModelGenerator.${props.biz}`, newValue);\n    }\n});\nonMounted(() => {\n    if (props.biz) {\n        selectedModel.value = StorageUtil.get(\n            `ModelGenerator.${props.biz}`,\n            \"\",\n        );\n    }\n});\n\nconst chat = async (\n    prompt: string,\n    chatParam: ChatParam,\n    param?: Record<string, any>,\n    option?: {\n        format?: \"text\" | \"json\";\n    },\n): Promise<ModelChatResult> => {\n    option = Object.assign(\n        {\n            format: \"text\",\n        },\n        option,\n    );\n    if (param) {\n        prompt = StringUtil.replaceParam(prompt, param);\n        if (chatParam.systemPrompt) {\n            chatParam.systemPrompt = StringUtil.replaceParam(\n                chatParam.systemPrompt,\n                param,\n            );\n        }\n    }\n    const [providerId, modelId] = (selectedModel.value || \"|\").split(\"|\");\n    const ret = await modelStore.chat(providerId, modelId, prompt, chatParam);\n    if (ret.code) {\n        return ret;\n    }\n    if (option.format === \"json\") {\n        let content = ret.data!.content;\n        if (!content) {\n            ret.code = -1;\n            ret.msg = t(\"error.responseEmpty\");\n            return ret;\n        }\n        content = content.trim();\n        // ```json xxx ``` replace\n        if (/^```json/.test(content)) {\n            content = content\n                .replace(/^```json/, \"\")\n                .replace(/```$/, \"\")\n                .trim();\n        } else if (/^```/.test(content)) {\n            content = content.replace(/^```/, \"\").replace(/```$/, \"\").trim();\n        }\n        try {\n            ret.data!.json = JSON.parse(content);\n        } catch (e) {\n            ret.code = -1;\n            ret.msg = t(\"error.parseFailed\") + \":\" + content;\n        }\n    }\n    return ret;\n};\ndefineExpose({\n    chat,\n});\n</script>\n\n<template>\n    <ModelSelector v-model=\"selectedModel\" />\n</template>\n"
  },
  {
    "path": "src/module/Model/ModelPromptDataConfigButton.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { doCopy } from \"../../components/common/util\";\nimport { t } from \"../../lang\";\nimport { Dialog } from \"../../lib/dialog\";\n\nconst props = defineProps<{\n    size?: \"small\" | undefined;\n    title: string;\n    name: string;\n    defaultPrompt: string;\n    defaultSystemPrompt?: string;\n    promptPlaceholder?: string;\n    systemPromptPlaceholder?: string;\n    enableSystemPrompt?: boolean;\n    help?: string;\n    param?: Record<string, string> | { name: string; label: string }[];\n}>();\nconst visible = ref(false);\nconst prompt = ref<string>(\"\");\nconst systemPrompt = ref<string>(\"\");\nconst doShow = async () => {\n    visible.value = true;\n    prompt.value = (await $mapi.storage.get(\n        \"data\",\n        props.name,\n        props.defaultPrompt,\n    )) as string;\n    if (props.enableSystemPrompt) {\n        systemPrompt.value = (await $mapi.storage.get(\n            \"data\",\n            props.name + \"System\",\n            props.defaultSystemPrompt,\n        )) as string;\n    }\n};\nconst doSave = () => {\n    visible.value = false;\n    $mapi.storage.set(\"data\", props.name, prompt.value);\n    if (props.enableSystemPrompt) {\n        $mapi.storage.set(\"data\", props.name + \"System\", systemPrompt.value);\n    }\n    Dialog.tipSuccess(t(\"common.saveSuccess\"));\n};\nconst doRestore = () => {\n    prompt.value = props.defaultPrompt;\n    $mapi.storage.set(\"data\", props.name, prompt.value);\n    if (props.enableSystemPrompt) {\n        systemPrompt.value = props.defaultSystemPrompt || \"\";\n        $mapi.storage.set(\"data\", props.name + \"System\", systemPrompt.value);\n    }\n};\n</script>\n\n<template>\n    <a-button @click=\"doShow()\" :size=\"size\">\n        <template #icon>\n            <icon-file />\n        </template>\n        {{ title }}\n    </a-button>\n    <a-modal v-model:visible=\"visible\" width=\"800px\" title-align=\"start\">\n        <template #title>\n            {{ title }}\n        </template>\n        <template #footer>\n            <a-button @click=\"doRestore\">\n                {{ $t(\"common.restoreDefault\") }}\n            </a-button>\n            <a-button type=\"primary\" @click=\"doSave\">\n                {{ $t(\"common.save\") }}\n            </a-button>\n        </template>\n        <div class=\"-mx- -my-3\" style=\"height: 60vh\">\n            <slot></slot>\n            <div v-if=\"help\">\n                <a-alert type=\"info\" show-icon class=\"mb-2\">\n                    {{ help }}\n                </a-alert>\n            </div>\n            <div v-if=\"enableSystemPrompt\">\n                <div class=\"text-sm mb-1\">{{ $t(\"model.systemPrompt\") }}</div>\n                <div>\n                    <a-textarea\n                        v-model=\"systemPrompt\"\n                        :placeholder=\"systemPromptPlaceholder\"\n                        :auto-size=\"{ minRows: 10, maxRows: 15 }\"\n                    />\n                </div>\n            </div>\n            <div>\n                <div v-if=\"enableSystemPrompt\" class=\"text-sm mb-1\">\n                    {{ $t(\"model.userPrompt\") }}\n                </div>\n                <div>\n                    <a-textarea\n                        v-model=\"prompt\"\n                        :placeholder=\"promptPlaceholder\"\n                        :auto-size=\"{ minRows: 10, maxRows: 15 }\"\n                    />\n                </div>\n            </div>\n            <div v-if=\"props.param\">\n                <div class=\"mt-2 font-bold\">\n                    {{ $t(\"common.availableVars\") }}:\n                </div>\n                <div v-if=\"Array.isArray(props.param)\" class=\"mt-1\">\n                    <div\n                        v-for=\"item in props.param as {\n                            name: string;\n                            label: string;\n                        }[]\"\n                        :key=\"item.name\"\n                        class=\"mr-4 inline-flex items-center text-xs\"\n                    >\n                        <div\n                            class=\"font-mono mr-1 cursor-pointer\"\n                            @click=\"doCopy(`{${item.name}}`)\"\n                        >\n                            {{ \"{\" + item.name + \"}\" }}\n                        </div>\n                        <div class=\"text-gray-400\">\n                            {{ item.label }}\n                        </div>\n                    </div>\n                </div>\n                <div class=\"mt-1\" v-else>\n                    <div\n                        v-for=\"(value, key) in props.param\"\n                        :key=\"key\"\n                        class=\"mr-4 inline-flex items-center text-xs\"\n                    >\n                        <div\n                            class=\"font-mono mr-1 cursor-pointer\"\n                            @click=\"doCopy(`{${key}}`)\"\n                        >\n                            {{ \"{\" + key + \"}\" }}\n                        </div>\n                        <div class=\"text-gray-400\">\n                            {{ value }}\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"h-4\"></div>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/module/Model/ModelSelector.vue",
    "content": "<script setup lang=\"ts\">\nimport { useModelStore } from \"./store/model\";\nimport { computed, ref } from \"vue\";\nimport { getModelLogo } from \"./models\";\n\ntype ModelRecord = {\n    providerId: string;\n    providerTitle: string;\n    modelId: string;\n    modelName: string;\n};\nconst model = useModelStore();\nconst availableModels = computed(() => {\n    const models: ModelRecord[] = [];\n    for (const p of model.providers) {\n        if (!p.data.enabled) {\n            continue;\n        }\n        for (const m of p.data.models) {\n            if (!m.enabled) {\n                continue;\n            }\n            models.push({\n                providerId: p.id,\n                providerTitle: p.title,\n                modelId: m.id,\n                modelName: m.name,\n            } as ModelRecord);\n        }\n    }\n    return models;\n});\nconst select = ref<any>(null);\nconst selectedProvider = computed(() => {\n    if (select.value?.modelValue) {\n        const [providerId, modelId] = select.value.modelValue.split(\"|\");\n        for (const p of model.providers) {\n            if (p.id === providerId) {\n                return p;\n            }\n        }\n    } else {\n        return null;\n    }\n});\nconst selectedModel = computed(() => {\n    const [providerId, modelId] = select.value.modelValue.split(\"|\");\n    if (!selectedProvider.value) {\n        return null;\n    }\n    for (const m of selectedProvider.value.data.models) {\n        if (m.id === modelId) {\n            return m;\n        }\n    }\n    return null;\n});\ndefineExpose({\n    getInfo: () => {\n        return {\n            providerLogo: getModelLogo(selectedModel.value?.id || \"\"),\n            providerTitle: selectedProvider.value?.title || \"\",\n            modelName: selectedModel.value?.name || \"\",\n        };\n    },\n});\n</script>\n\n<template>\n    <a-select\n        ref=\"select\"\n        style=\"width: auto\"\n        :placeholder=\"$t('model.select')\"\n    >\n        <template #label>\n            <div\n                class=\"flex items-center\"\n                v-if=\"selectedProvider && selectedModel\"\n            >\n                <div class=\"mr-1\">\n                    <a-avatar\n                        :image-url=\"getModelLogo(selectedModel.id)\"\n                        :size=\"20\"\n                        shape=\"square\"\n                        style=\"border: 1px solid #ccc\"\n                    />\n                </div>\n                <div class=\"mr-1\">\n                    {{ selectedProvider?.title }}\n                </div>\n                <div class=\"mr-1 text-gray-400\">/</div>\n                <div>\n                    {{ selectedModel.name }}\n                </div>\n            </div>\n            <div class=\"flex items-center\" v-else>\n                <div class=\"mr-1\">\n                    {{ $t(\"model.select\") }}\n                </div>\n            </div>\n        </template>\n        <a-option\n            v-for=\"p in availableModels\"\n            :value=\"p.providerId + '|' + p.modelId\"\n        >\n            <div class=\"flex items-center\">\n                <div class=\"mr-1\">\n                    <a-avatar\n                        :image-url=\"getModelLogo(p.modelId)\"\n                        :size=\"20\"\n                        shape=\"square\"\n                        style=\"border: 1px solid #ccc\"\n                    />\n                </div>\n                <div class=\"mr-1\">\n                    {{ p.providerTitle }}\n                </div>\n                <div class=\"mr-1 text-gray-400\">/</div>\n                <div>\n                    {{ p.modelName }}\n                </div>\n            </div>\n        </a-option>\n    </a-select>\n</template>\n"
  },
  {
    "path": "src/module/Model/ModelSetting.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useSettingStore } from \"../../store/modules/setting\";\nimport { useUserStore } from \"../../store/modules/user\";\nimport ModelAddDialog from \"./components/ModelAddDialog.vue\";\nimport ModelEditDialog from \"./components/ModelEditDialog.vue\";\nimport ProviderAddDialog from \"./components/ProviderAddDialog.vue\";\nimport ProviderEditDialog from \"./components/ProviderEditDialog.vue\";\nimport ProviderTestDialog from \"./components/ProviderTestDialog.vue\";\nimport { getModelLogo } from \"./models\";\nimport { getProviderUrl } from \"./providers\";\nimport { useModelStore } from \"./store/model\";\n\nconst userStore = useUserStore();\nconst setting = useSettingStore();\nconst modelStore = useModelStore();\nconst providerAdd = ref<InstanceType<typeof ProviderAddDialog> | null>(null);\nconst providerEdit = ref<InstanceType<typeof ProviderEditDialog> | null>(null);\nconst modelAdd = ref<InstanceType<typeof ModelAddDialog> | null>(null);\nconst modelEdit = ref<InstanceType<typeof ModelEditDialog> | null>(null);\nconst providerTest = ref<InstanceType<typeof ProviderTestDialog> | null>(null);\nconst doUser = async () => {\n    if (!setting.basic.userEnable) {\n        return;\n    }\n    await window.$mapi.user.open({\n        readyParam: {\n            page: \"ChargeLmApi\",\n        },\n    });\n};\n\nconst keywords = ref(\"\");\nconst currentProviderId = ref(\"\");\n\nconst doSelectProvider = (id: string) => {\n    currentProviderId.value = id;\n};\nconst provider = computed(() => {\n    return modelStore.providers.find((p) => p.id === currentProviderId.value);\n});\nconst providerUrl = computed(() => {\n    if (!provider.value) return \"\";\n    return getProviderUrl(provider.value);\n});\nconst providerModelGroups = computed(() => {\n    if (!provider.value) {\n        return [];\n    }\n    const models = provider.value.data.models;\n    const groups = models\n        .map((m) => m.group)\n        .filter((v, i, a) => a.indexOf(v) === i);\n    return groups.map((g) => {\n        return {\n            group: g,\n            models: models.filter((m) => m.group === g),\n        };\n    });\n});\nconst providersFilter = computed(() => {\n    return modelStore.providers.filter((p) => {\n        if (keywords.value) {\n            return p.title.toLowerCase().includes(keywords.value.toLowerCase());\n        }\n        return true;\n    });\n});\nwatch(\n    () => modelStore.providers,\n    () => {\n        if (!currentProviderId.value && modelStore.providers.length > 0) {\n            doSelectProvider(modelStore.providers[0].id);\n        }\n    },\n    { immediate: true },\n);\n</script>\n\n<template>\n    <div class=\"flex h-full\">\n        <div class=\"w-48 border-r flex flex-col flex-shrink-0\">\n            <div class=\"p-2\">\n                <a-input\n                    :placeholder=\"$t('model.searchPlatform')\"\n                    v-model=\"keywords\"\n                >\n                    <template #suffix>\n                        <icon-search />\n                    </template>\n                </a-input>\n            </div>\n            <div class=\"flex-grow p-2 overflow-x-hidden overflow-y-auto\">\n                <div\n                    v-if=\"\n                        !providersFilter.length && modelStore.providers.length\n                    \"\n                >\n                    <a-empty :description=\"$t('empty.noModelPlatform')\" />\n                </div>\n                <div v-for=\"p in providersFilter\">\n                    <div\n                        class=\"flex hover:bg-gray-100 cursor-pointer border border-transparent rounded-full mb-3 px-3 py-1 items-center\"\n                        :class=\"\n                            currentProviderId === p.id\n                                ? 'bg-gray-100 border-gray-300'\n                                : ''\n                        \"\n                        @click=\"doSelectProvider(p.id)\"\n                    >\n                        <div class=\"mr-2\">\n                            <img\n                                v-if=\"p.logo\"\n                                class=\"w-[20px] h-[20px] rounded\"\n                                style=\"background: #eee; border: 1px solid #eee\"\n                                :src=\"p.logo\"\n                            />\n                            <a-avatar\n                                v-else\n                                :size=\"20\"\n                                shape=\"square\"\n                                :style=\"{ backgroundColor: '#3370ff' }\"\n                            >\n                                {{ p.title }}\n                            </a-avatar>\n                        </div>\n                        <div class=\"flex-grow\">\n                            {{ p.title }}\n                        </div>\n                        <div v-if=\"p.data.enabled\">\n                            <icon-check-circle class=\"text-green-600\" />\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"p-2\">\n                <a-button class=\"w-full\" @click=\"providerAdd?.show()\">\n                    {{ $t(\"common.add\") }}\n                    <template #icon>\n                        <icon-plus />\n                    </template>\n                </a-button>\n            </div>\n        </div>\n        <div class=\"flex-grow overflow-y-auto overflow-x-hidden\">\n            <div class=\"py-20\" v-if=\"!provider\">\n                <a-empty :description=\"$t('hint.selectPlatform')\" />\n            </div>\n            <div v-else class=\"p-3\">\n                <div class=\"flex items-center border-b pb-3 mb-3\">\n                    <div class=\"font-bold mr-2\">\n                        {{ provider.title }}\n                    </div>\n                    <div class=\"flex-grow\">\n                        <a\n                            v-if=\"!provider.isSystem\"\n                            href=\"javascript:;\"\n                            class=\"mr-2\"\n                            @click=\"providerEdit?.show(provider)\"\n                        >\n                            <icon-edit />\n                        </a>\n                        <a\n                            v-if=\"provider?.websites.official\"\n                            class=\"mr-2\"\n                            target=\"_blank\"\n                            :href=\"provider?.websites.official\"\n                        >\n                            <icon-desktop />\n                        </a>\n                    </div>\n                    <div>\n                        <a-switch\n                            :model-value=\"provider.data.enabled\"\n                            @change=\"\n                                modelStore.change(\n                                    provider.id,\n                                    'data.enabled',\n                                    $event,\n                                )\n                            \"\n                        />\n                    </div>\n                </div>\n                <div class=\"mb-3\" v-if=\"provider.id !== 'buildIn'\">\n                    <div class=\"mb-2 font-bold\">{{ $t(\"setting.apiKey\") }}</div>\n                    <div>\n                        <a-input-password\n                            :model-value=\"provider.data.apiKey\"\n                            @input=\"\n                                modelStore.change(\n                                    provider.id,\n                                    'data.apiKey',\n                                    $event,\n                                )\n                            \"\n                            class=\"w-full\"\n                        >\n                            <template #suffix>\n                                <a\n                                    href=\"javascript:;\"\n                                    @click=\"providerTest?.show()\"\n                                    class=\"ml-2\"\n                                >\n                                    {{ $t(\"common.check\") }}\n                                </a>\n                            </template>\n                        </a-input-password>\n                    </div>\n                </div>\n                <div class=\"mb-3\" v-if=\"provider.id !== 'buildIn'\">\n                    <div class=\"mb-2 font-bold\">{{ $t(\"setting.apiUrl\") }}</div>\n                    <div>\n                        <a-input\n                            :model-value=\"provider.data.apiHost\"\n                            @input=\"\n                                modelStore.change(\n                                    provider.id,\n                                    'data.apiHost',\n                                    $event,\n                                )\n                            \"\n                            class=\"w-full\"\n                        >\n                        </a-input>\n                    </div>\n                    <div class=\"flex\">\n                        <div class=\"text-gray-400\">\n                            {{ providerUrl }}\n                        </div>\n                    </div>\n                </div>\n                <div\n                    class=\"mb-3 flex border rounded p-3 items-center\"\n                    v-if=\"provider.id === 'buildIn'\"\n                >\n                    <div class=\"flex-grow\">\n                        {{ $t(\"user.energy\") }}\n                        <span class=\"font-bold\"\n                            >{{\n                                (\n                                    (userStore.data.lmApi?.quota || 0) / 1000\n                                ).toFixed(2)\n                            }}K</span\n                        >\n                    </div>\n                    <div class=\"text-gray-400\">\n                        <icon-check class=\"text-green-600\" />\n                        {{ $t(\"model.builtinDesc\") }}\n                    </div>\n                    <div>\n                        <a-button class=\"ml-2\" @click=\"doUser\">\n                            {{ $t(\"common.recharge\") }}\n                        </a-button>\n                    </div>\n                </div>\n                <div class=\"mb-3\">\n                    <div class=\"mb-2 font-bold\">{{ $t(\"model.model\") }}</div>\n                    <div\n                        class=\"mb-2 text-sm text-gray-400\"\n                        v-if=\"provider.id !== 'buildIn'\"\n                    >\n                        {{ $t(\"common.view\") }}\n                        <a\n                            :href=\"provider?.websites.docs\"\n                            target=\"_blank\"\n                            class=\"text-blue-600\"\n                        >\n                            {{ provider.title }}\n                            {{ $t(\"common.docs\") }}\n                        </a>\n                        {{ $t(\"common.and\") }}\n                        <a\n                            :href=\"provider?.websites.models\"\n                            target=\"_blank\"\n                            class=\"text-blue-600\"\n                        >\n                            {{ provider.title }}\n                            {{ $t(\"model.list\") }}\n                        </a>\n                        {{ $t(\"common.moreDetails\") }}\n                    </div>\n                    <div\n                        v-for=\"g in providerModelGroups\"\n                        :key=\"provider.id + g.group\"\n                        class=\"mb-2\"\n                    >\n                        <a-collapse :default-active-key=\"[g.group]\">\n                            <a-collapse-item\n                                :header=\"$t(g.group)\"\n                                :key=\"g.group\"\n                            >\n                                <div class=\"-ml-6 -mr-1\">\n                                    <div\n                                        v-for=\"m in g.models\"\n                                        class=\"border mb-3 rounded-lg bg-white flex p-2 items-center\"\n                                    >\n                                        <div class=\"mr-2\">\n                                            <a-avatar\n                                                :image-url=\"getModelLogo(m.id)\"\n                                                :size=\"20\"\n                                                shape=\"square\"\n                                                style=\"border: 1px solid #ccc\"\n                                            />\n                                        </div>\n                                        <div class=\"flex-grow\">\n                                            {{ m.name }}\n                                        </div>\n                                        <div class=\"flex items-center\">\n                                            <a-switch\n                                                v-if=\"provider.id !== 'buildIn'\"\n                                                :model-value=\"m.enabled\"\n                                                @change=\"\n                                                    modelStore.changeModel(\n                                                        provider.id,\n                                                        m.id,\n                                                        'enabled',\n                                                        $event,\n                                                    )\n                                                \"\n                                                class=\"mr-2\"\n                                            ></a-switch>\n                                            <a-button\n                                                @click=\"modelEdit?.show(m)\"\n                                                v-if=\"provider.id !== 'buildIn'\"\n                                                class=\"mr-2\"\n                                            >\n                                                <template #icon>\n                                                    <icon-settings />\n                                                </template>\n                                            </a-button>\n                                            <a-button\n                                                @click=\"\n                                                    modelStore.modelDelete(\n                                                        provider.id,\n                                                        m.id,\n                                                    )\n                                                \"\n                                                v-if=\"provider.id !== 'buildIn'\"\n                                            >\n                                                <template #icon>\n                                                    <icon-delete />\n                                                </template>\n                                            </a-button>\n                                        </div>\n                                    </div>\n                                </div>\n                            </a-collapse-item>\n                        </a-collapse>\n                    </div>\n                    <div class=\"mb-2\" v-if=\"provider.id !== 'buildIn'\">\n                        <a-button @click=\"modelAdd?.show()\">\n                            <template #icon>\n                                <icon-plus />\n                            </template>\n                            {{ $t(\"common.add\") }}\n                        </a-button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n    <ProviderAddDialog ref=\"providerAdd\" />\n    <ProviderEditDialog ref=\"providerEdit\" />\n    <ModelAddDialog ref=\"modelAdd\" :provider=\"provider\" />\n    <ModelEditDialog ref=\"modelEdit\" :provider=\"provider\" />\n    <ProviderTestDialog ref=\"providerTest\" :provider=\"provider\" />\n</template>\n"
  },
  {
    "path": "src/module/Model/ModelSettingDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport ModelSetting from \"./ModelSetting.vue\";\n\nconst visible = ref(false);\nconst show = () => {\n    visible.value = true;\n};\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"50rem\"\n        :footer=\"false\"\n        :esc-to-close=\"false\"\n        :mask-closable=\"false\"\n        title-align=\"start\"\n    >\n        <template #title>\n            {{ $t(\"setting.llm\") }}\n        </template>\n        <div class=\"-mx-5 -my-6\" style=\"height: calc(100vh - 15rem)\">\n            <ModelSetting />\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/module/Model/components/ModelAddDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, watch } from \"vue\";\nimport { useModelStore } from \"../store/model\";\n\nconst modelStore = useModelStore();\nconst props = defineProps({\n    provider: {\n        type: Object,\n        default: () => {\n            return null;\n        },\n    },\n});\nconst visible = ref(false);\nconst data = ref({\n    id: \"\",\n    name: \"\",\n    group: \"\",\n});\nwatch(\n    () => data.value.id,\n    (val) => {\n        data.value.name = data.value.id;\n        data.value.group = data.value.id;\n    },\n);\nconst show = () => {\n    data.value.id = \"\";\n    data.value.name = \"\";\n    data.value.group = \"\";\n    visible.value = true;\n};\nconst doSubmit = () => {\n    if (!data.value.id) {\n        return;\n    }\n    modelStore.modelAdd(props.provider.id, data.value);\n    visible.value = false;\n};\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"30rem\"\n        :esc-to-close=\"false\"\n        :mask-closable=\"false\"\n        title-align=\"start\"\n    >\n        <template #title>\n            {{ $t(\"model.add\") }}\n        </template>\n        <template #footer>\n            <a-button @click=\"visible = false\">{{\n                $t(\"common.cancel\")\n            }}</a-button>\n            <a-button type=\"primary\" @click=\"doSubmit\">{{\n                $t(\"common.confirm\")\n            }}</a-button>\n        </template>\n        <div style=\"max-height: 50vh\" class=\"overflow-y-auto\">\n            <a-form :model=\"data\" label-align=\"left\" class=\"mt-4\">\n                <a-form-item :label=\"$t('model.id')\" name=\"title\" required>\n                    <a-input\n                        v-model:model-value=\"data.id\"\n                        :placeholder=\"$t('placeholder.requiredGpt')\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('model.name')\" name=\"title\">\n                    <a-input\n                        v-model:model-value=\"data.name\"\n                        :placeholder=\"$t('placeholder.gpt35')\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('group.name')\" name=\"type\">\n                    <a-input\n                        v-model:model-value=\"data.group\"\n                        :placeholder=\"$t('placeholder.chatgpt')\"\n                    />\n                </a-form-item>\n            </a-form>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/module/Model/components/ModelEditDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useModelStore } from \"../store/model\";\nimport { Model } from \"../types\";\n\nconst modelStore = useModelStore();\nconst props = defineProps({\n    provider: {\n        type: Object,\n        default: () => {\n            return null;\n        },\n    },\n});\nconst visible = ref(false);\nconst data = ref({\n    id: \"\",\n    name: \"\",\n    group: \"\",\n});\nconst show = (model: Model) => {\n    data.value.id = model.id;\n    data.value.name = model.name;\n    data.value.group = model.group;\n    visible.value = true;\n};\nconst doSubmit = () => {\n    if (!data.value.id) {\n        return;\n    }\n    modelStore.modelEdit(props.provider.id, data.value);\n    visible.value = false;\n};\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"30rem\"\n        :esc-to-close=\"false\"\n        :mask-closable=\"false\"\n        title-align=\"start\"\n    >\n        <template #title>\n            {{ $t(\"model.edit\") }}\n        </template>\n        <template #footer>\n            <a-button @click=\"visible = false\">{{\n                $t(\"common.cancel\")\n            }}</a-button>\n            <a-button type=\"primary\" @click=\"doSubmit\">{{\n                $t(\"common.confirm\")\n            }}</a-button>\n        </template>\n        <div style=\"max-height: 50vh\" class=\"overflow-y-auto\">\n            <a-form :model=\"data\" label-align=\"left\" class=\"mt-4\">\n                <a-form-item :label=\"$t('model.id')\" name=\"title\" required>\n                    <a-input\n                        v-model:model-value=\"data.id\"\n                        readonly\n                        disabled\n                        :placeholder=\"$t('placeholder.requiredGpt')\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('model.name')\" name=\"title\">\n                    <a-input\n                        v-model:model-value=\"data.name\"\n                        :placeholder=\"$t('placeholder.gpt35')\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('group.name')\" name=\"type\">\n                    <a-input\n                        v-model:model-value=\"data.group\"\n                        :placeholder=\"$t('placeholder.chatgpt')\"\n                    />\n                </a-form-item>\n            </a-form>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/module/Model/components/ProviderAddDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { ProviderType } from \"../types\";\nimport { useModelStore } from \"../store/model\";\n\nconst modelStore = useModelStore();\nconst visible = ref(false);\nconst data = ref({\n    title: \"\",\n    type: \"openai\" as ProviderType,\n});\nconst show = () => {\n    data.value.title = \"\";\n    data.value.type = \"openai\";\n    visible.value = true;\n};\nconst doSubmit = () => {\n    if (!data.value.title) {\n        return;\n    }\n    modelStore.add(data.value);\n    visible.value = false;\n};\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"30rem\"\n        :esc-to-close=\"false\"\n        :mask-closable=\"false\"\n        title-align=\"start\"\n    >\n        <template #title>\n            {{ $t(\"model.addProvider\") }}\n        </template>\n        <template #footer>\n            <a-button @click=\"visible = false\">{{\n                $t(\"common.cancel\")\n            }}</a-button>\n            <a-button type=\"primary\" @click=\"doSubmit\">{{\n                $t(\"common.confirm\")\n            }}</a-button>\n        </template>\n        <div style=\"max-height: 50vh\" class=\"overflow-y-auto\">\n            <a-form :model=\"data\" label-align=\"left\" class=\"mt-4\">\n                <a-form-item :label=\"$t('video.providerName')\" name=\"title\">\n                    <a-input\n                        v-model:model-value=\"data.title\"\n                        :placeholder=\"$t('video.providerName')\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('setting.interfaceType')\" name=\"type\">\n                    <a-select\n                        v-model:model-value=\"data.type\"\n                        :placeholder=\"$t('setting.interfaceType')\"\n                    >\n                        <a-option value=\"openai\">OpenAI</a-option>\n                    </a-select>\n                </a-form-item>\n            </a-form>\n        </div>\n    </a-modal>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/module/Model/components/ProviderEditDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { ProviderType } from \"../types\";\nimport { useModelStore } from \"../store/model\";\n\nconst modelStore = useModelStore();\nconst visible = ref(false);\nconst data = ref({\n    id: \"\",\n    title: \"\",\n    type: \"openai\" as ProviderType,\n});\nconst show = (provider) => {\n    data.value.id = provider.id;\n    data.value.title = provider.title;\n    data.value.type = provider.type;\n    visible.value = true;\n};\nconst doSubmit = () => {\n    if (!data.value.title) {\n        return;\n    }\n    modelStore.edit(data.value);\n    visible.value = false;\n};\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"30rem\"\n        :esc-to-close=\"false\"\n        :mask-closable=\"false\"\n        title-align=\"start\"\n    >\n        <template #title>\n            {{ $t(\"model.editProvider\") }}\n        </template>\n        <template #footer>\n            <a-button @click=\"visible = false\">{{\n                $t(\"common.cancel\")\n            }}</a-button>\n            <a-button type=\"primary\" @click=\"doSubmit\">{{\n                $t(\"common.confirm\")\n            }}</a-button>\n        </template>\n        <div style=\"max-height: 50vh\" class=\"overflow-y-auto\">\n            <a-form :model=\"data\" label-align=\"left\" class=\"mt-4\">\n                <a-form-item :label=\"$t('video.providerName')\" name=\"title\">\n                    <a-input\n                        v-model:model-value=\"data.title\"\n                        :placeholder=\"$t('video.providerName')\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('setting.interfaceType')\" name=\"type\">\n                    <a-select\n                        v-model:model-value=\"data.type\"\n                        :placeholder=\"$t('setting.interfaceType')\"\n                    >\n                        <a-option value=\"openai\">OpenAI</a-option>\n                    </a-select>\n                </a-form-item>\n            </a-form>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/module/Model/components/ProviderTestDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useModelStore } from \"../store/model\";\nimport { Model } from \"../types\";\n\nconst modelStore = useModelStore();\nconst props = defineProps({\n    provider: {\n        type: Object,\n        default: () => {\n            return null;\n        },\n    },\n});\nconst visible = ref(false);\nconst data = ref({\n    modelId: \"\",\n});\nconst enabledModels = computed(() => {\n    if (props.provider) {\n        return props.provider.data.models.filter(\n            (model: Model) => model.enabled,\n        );\n    }\n    return [];\n});\nwatch(enabledModels, (newVal) => {\n    let exists = false;\n    for (const model of newVal) {\n        if (model.id === data.value.modelId) {\n            exists = true;\n            break;\n        }\n    }\n    if (!exists) {\n        if (newVal.length > 0) {\n            data.value.modelId = newVal[0].id;\n        } else {\n            data.value.modelId = \"\";\n        }\n    }\n});\nconst show = () => {\n    visible.value = true;\n};\nconst doSubmit = async () => {\n    if (!data.value.modelId) {\n        return;\n    }\n    await modelStore.test(props.provider.id, data.value.modelId);\n};\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal\n        v-model:visible=\"visible\"\n        width=\"20rem\"\n        :esc-to-close=\"false\"\n        :mask-closable=\"false\"\n        title-align=\"start\"\n    >\n        <template #title>\n            {{ $t(\"hint.selectModelCheck\") }}\n        </template>\n        <template #footer>\n            <a-button type=\"primary\" @click=\"doSubmit\">{{\n                $t(\"common.test\")\n            }}</a-button>\n            <a-button @click=\"visible = false\">{{\n                $t(\"common.close\")\n            }}</a-button>\n        </template>\n        <div\n            style=\"max-height: 50vh\"\n            class=\"overflow-y-auto\"\n            v-if=\"props.provider\"\n        >\n            <a-form :model=\"data\" layout=\"vertical\" class=\"mt-4\">\n                <a-form-item name=\"modelId\">\n                    <a-select v-model:model-value=\"data.modelId\">\n                        <a-option\n                            v-for=\"model in enabledModels\"\n                            :key=\"model.id\"\n                            :value=\"model.id\"\n                        >\n                            {{ model.name }}\n                        </a-option>\n                    </a-select>\n                </a-form-item>\n            </a-form>\n        </div>\n    </a-modal>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/module/Model/models.ts",
    "content": "import Ai360ModelLogo from \"./assets/image/models/360.png\";\nimport Ai360ModelLogoDark from \"./assets/image/models/360_dark.png\";\nimport AdeptModelLogo from \"./assets/image/models/adept.png\";\nimport AdeptModelLogoDark from \"./assets/image/models/adept_dark.png\";\nimport Ai21ModelLogo from \"./assets/image/models/ai21.png\";\nimport Ai21ModelLogoDark from \"./assets/image/models/ai21_dark.png\";\nimport AimassModelLogo from \"./assets/image/models/aimass.png\";\nimport AimassModelLogoDark from \"./assets/image/models/aimass_dark.png\";\nimport AisingaporeModelLogo from \"./assets/image/models/aisingapore.png\";\nimport AisingaporeModelLogoDark from \"./assets/image/models/aisingapore_dark.png\";\nimport BaichuanModelLogo from \"./assets/image/models/baichuan.png\";\nimport BaichuanModelLogoDark from \"./assets/image/models/baichuan_dark.png\";\nimport BgeModelLogo from \"./assets/image/models/bge.webp\";\nimport BigcodeModelLogo from \"./assets/image/models/bigcode.webp\";\nimport BigcodeModelLogoDark from \"./assets/image/models/bigcode_dark.webp\";\nimport ChatGLMModelLogo from \"./assets/image/models/chatglm.png\";\nimport ChatGLMModelLogoDark from \"./assets/image/models/chatglm_dark.png\";\nimport ChatGptModelLogo from \"./assets/image/models/chatgpt.jpeg\";\nimport ClaudeModelLogo from \"./assets/image/models/claude.png\";\nimport ClaudeModelLogoDark from \"./assets/image/models/claude_dark.png\";\nimport CodegeexModelLogo from \"./assets/image/models/codegeex.png\";\nimport CodegeexModelLogoDark from \"./assets/image/models/codegeex_dark.png\";\nimport CodestralModelLogo from \"./assets/image/models/codestral.png\";\nimport CohereModelLogo from \"./assets/image/models/cohere.png\";\nimport CohereModelLogoDark from \"./assets/image/models/cohere_dark.png\";\nimport CopilotModelLogo from \"./assets/image/models/copilot.png\";\nimport CopilotModelLogoDark from \"./assets/image/models/copilot_dark.png\";\nimport DalleModelLogo from \"./assets/image/models/dalle.png\";\nimport DalleModelLogoDark from \"./assets/image/models/dalle_dark.png\";\nimport DbrxModelLogo from \"./assets/image/models/dbrx.png\";\nimport DeepSeekModelLogo from \"./assets/image/models/deepseek.png\";\nimport DeepSeekModelLogoDark from \"./assets/image/models/deepseek_dark.png\";\nimport DianxinModelLogo from \"./assets/image/models/dianxin.png\";\nimport DianxinModelLogoDark from \"./assets/image/models/dianxin_dark.png\";\nimport DoubaoModelLogo from \"./assets/image/models/doubao.png\";\nimport DoubaoModelLogoDark from \"./assets/image/models/doubao_dark.png\";\nimport {\n    default as EmbeddingModelLogo,\n    default as EmbeddingModelLogoDark,\n} from \"./assets/image/models/embedding.png\";\nimport FlashaudioModelLogo from \"./assets/image/models/flashaudio.png\";\nimport FlashaudioModelLogoDark from \"./assets/image/models/flashaudio_dark.png\";\nimport FluxModelLogo from \"./assets/image/models/flux.png\";\nimport FluxModelLogoDark from \"./assets/image/models/flux_dark.png\";\nimport GeminiModelLogo from \"./assets/image/models/gemini.png\";\nimport GeminiModelLogoDark from \"./assets/image/models/gemini_dark.png\";\nimport GemmaModelLogo from \"./assets/image/models/gemma.png\";\nimport GemmaModelLogoDark from \"./assets/image/models/gemma_dark.png\";\nimport {\n    default as GoogleModelLogo,\n    default as GoogleModelLogoDark,\n} from \"./assets/image/models/google.png\";\nimport ChatGPT35ModelLogo from \"./assets/image/models/gpt_3.5.png\";\nimport ChatGPT4ModelLogo from \"./assets/image/models/gpt_4.png\";\nimport {\n    default as ChatGPT35ModelLogoDark,\n    default as ChatGPT4ModelLogoDark,\n    default as ChatGptModelLogoDark,\n    default as ChatGPTo1ModelLogoDark,\n} from \"./assets/image/models/gpt_dark.png\";\nimport ChatGPTo1ModelLogo from \"./assets/image/models/gpt_o1.png\";\nimport GrokModelLogo from \"./assets/image/models/grok.png\";\nimport GrokModelLogoDark from \"./assets/image/models/grok_dark.png\";\nimport GrypheModelLogo from \"./assets/image/models/gryphe.png\";\nimport GrypheModelLogoDark from \"./assets/image/models/gryphe_dark.png\";\nimport HailuoModelLogo from \"./assets/image/models/hailuo.png\";\nimport HailuoModelLogoDark from \"./assets/image/models/hailuo_dark.png\";\nimport HuggingfaceModelLogo from \"./assets/image/models/huggingface.png\";\nimport HuggingfaceModelLogoDark from \"./assets/image/models/huggingface_dark.png\";\nimport HunyuanModelLogo from \"./assets/image/models/hunyuan.png\";\nimport HunyuanModelLogoDark from \"./assets/image/models/hunyuan_dark.png\";\nimport IbmModelLogo from \"./assets/image/models/ibm.png\";\nimport IbmModelLogoDark from \"./assets/image/models/ibm_dark.png\";\nimport InternlmModelLogo from \"./assets/image/models/internlm.png\";\nimport InternlmModelLogoDark from \"./assets/image/models/internlm_dark.png\";\nimport InternvlModelLogo from \"./assets/image/models/internvl.png\";\nimport JinaModelLogo from \"./assets/image/models/jina.png\";\nimport JinaModelLogoDark from \"./assets/image/models/jina_dark.png\";\nimport KeLingModelLogo from \"./assets/image/models/keling.png\";\nimport KeLingModelLogoDark from \"./assets/image/models/keling_dark.png\";\nimport LlamaModelLogo from \"./assets/image/models/llama.png\";\nimport LlamaModelLogoDark from \"./assets/image/models/llama_dark.png\";\nimport LLavaModelLogo from \"./assets/image/models/llava.png\";\nimport LLavaModelLogoDark from \"./assets/image/models/llava_dark.png\";\nimport LumaModelLogo from \"./assets/image/models/luma.png\";\nimport LumaModelLogoDark from \"./assets/image/models/luma_dark.png\";\nimport MagicModelLogo from \"./assets/image/models/magic.png\";\nimport MagicModelLogoDark from \"./assets/image/models/magic_dark.png\";\nimport MediatekModelLogo from \"./assets/image/models/mediatek.png\";\nimport MediatekModelLogoDark from \"./assets/image/models/mediatek_dark.png\";\nimport MicrosoftModelLogo from \"./assets/image/models/microsoft.png\";\nimport MicrosoftModelLogoDark from \"./assets/image/models/microsoft_dark.png\";\nimport MidjourneyModelLogo from \"./assets/image/models/midjourney.png\";\nimport MidjourneyModelLogoDark from \"./assets/image/models/midjourney_dark.png\";\nimport {\n    default as MinicpmModelLogo,\n    default as MinicpmModelLogoDark,\n} from \"./assets/image/models/minicpm.webp\";\nimport MinimaxModelLogo from \"./assets/image/models/minimax.png\";\nimport MinimaxModelLogoDark from \"./assets/image/models/minimax_dark.png\";\nimport MistralModelLogo from \"./assets/image/models/mixtral.png\";\nimport MistralModelLogoDark from \"./assets/image/models/mixtral_dark.png\";\nimport MoonshotModelLogo from \"./assets/image/models/moonshot.png\";\nimport MoonshotModelLogoDark from \"./assets/image/models/moonshot_dark.png\";\nimport {\n    default as NousResearchModelLogo,\n    default as NousResearchModelLogoDark,\n} from \"./assets/image/models/nousresearch.png\";\nimport NvidiaModelLogo from \"./assets/image/models/nvidia.png\";\nimport NvidiaModelLogoDark from \"./assets/image/models/nvidia_dark.png\";\nimport PalmModelLogo from \"./assets/image/models/palm.png\";\nimport PalmModelLogoDark from \"./assets/image/models/palm_dark.png\";\nimport {\n    default as PerplexityModelLogo,\n    default as PerplexityModelLogoDark,\n} from \"./assets/image/models/perplexity.png\";\nimport PixtralModelLogo from \"./assets/image/models/pixtral.png\";\nimport PixtralModelLogoDark from \"./assets/image/models/pixtral_dark.png\";\nimport QwenModelLogo from \"./assets/image/models/qwen.png\";\nimport QwenModelLogoDark from \"./assets/image/models/qwen_dark.png\";\nimport RakutenaiModelLogo from \"./assets/image/models/rakutenai.png\";\nimport RakutenaiModelLogoDark from \"./assets/image/models/rakutenai_dark.png\";\nimport SparkDeskModelLogo from \"./assets/image/models/sparkdesk.png\";\nimport SparkDeskModelLogoDark from \"./assets/image/models/sparkdesk_dark.png\";\nimport StabilityModelLogo from \"./assets/image/models/stability.png\";\nimport StabilityModelLogoDark from \"./assets/image/models/stability_dark.png\";\nimport StepModelLogo from \"./assets/image/models/step.png\";\nimport StepModelLogoDark from \"./assets/image/models/step_dark.png\";\nimport SunoModelLogo from \"./assets/image/models/suno.png\";\nimport SunoModelLogoDark from \"./assets/image/models/suno_dark.png\";\nimport TeleModelLogo from \"./assets/image/models/tele.png\";\nimport TeleModelLogoDark from \"./assets/image/models/tele_dark.png\";\nimport UpstageModelLogo from \"./assets/image/models/upstage.png\";\nimport UpstageModelLogoDark from \"./assets/image/models/upstage_dark.png\";\nimport ViduModelLogo from \"./assets/image/models/vidu.png\";\nimport ViduModelLogoDark from \"./assets/image/models/vidu_dark.png\";\nimport VoyageModelLogo from \"./assets/image/models/voyageai.png\";\nimport WenxinModelLogo from \"./assets/image/models/wenxin.png\";\nimport WenxinModelLogoDark from \"./assets/image/models/wenxin_dark.png\";\nimport XirangModelLogo from \"./assets/image/models/xirang.png\";\nimport XirangModelLogoDark from \"./assets/image/models/xirang_dark.png\";\nimport YiModelLogo from \"./assets/image/models/yi.png\";\nimport YiModelLogoDark from \"./assets/image/models/yi_dark.png\";\nimport { Model } from \"./types\";\n\nexport function getModelLogo(modelId: string) {\n    const isLight = true;\n\n    if (!modelId) {\n        return undefined;\n    }\n\n    const logoMap = {\n        pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark,\n        jina: isLight ? JinaModelLogo : JinaModelLogoDark,\n        abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,\n        minimax: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,\n        o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,\n        o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,\n        \"gpt-3\": isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark,\n        \"gpt-4\": isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,\n        gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,\n        \"text-moderation\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"babbage-\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"sora-\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"(^|/)omni-\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"Embedding-V1\": isLight ? WenxinModelLogo : WenxinModelLogoDark,\n        \"text-embedding-v\": isLight ? QwenModelLogo : QwenModelLogoDark,\n        \"text-embedding\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"davinci-\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,\n        deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,\n        \"(qwen|qwq-|qvq-)\": isLight ? QwenModelLogo : QwenModelLogoDark,\n        gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,\n        \"yi-\": isLight ? YiModelLogo : YiModelLogoDark,\n        llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,\n        mixtral: isLight ? MistralModelLogo : MistralModelLogo,\n        mistral: isLight ? MistralModelLogo : MistralModelLogoDark,\n        codestral: CodestralModelLogo,\n        ministral: isLight ? MistralModelLogo : MistralModelLogoDark,\n        moonshot: isLight ? MoonshotModelLogo : MoonshotModelLogoDark,\n        kimi: isLight ? MoonshotModelLogo : MoonshotModelLogoDark,\n        phi: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,\n        baichuan: isLight ? BaichuanModelLogo : BaichuanModelLogoDark,\n        claude: isLight ? ClaudeModelLogo : ClaudeModelLogoDark,\n        gemini: isLight ? GeminiModelLogo : GeminiModelLogoDark,\n        bison: isLight ? PalmModelLogo : PalmModelLogoDark,\n        palm: isLight ? PalmModelLogo : PalmModelLogoDark,\n        step: isLight ? StepModelLogo : StepModelLogoDark,\n        hailuo: isLight ? HailuoModelLogo : HailuoModelLogoDark,\n        doubao: isLight ? DoubaoModelLogo : DoubaoModelLogoDark,\n        \"ep-202\": isLight ? DoubaoModelLogo : DoubaoModelLogoDark,\n        cohere: isLight ? CohereModelLogo : CohereModelLogoDark,\n        command: isLight ? CohereModelLogo : CohereModelLogoDark,\n        minicpm: isLight ? MinicpmModelLogo : MinicpmModelLogoDark,\n        \"360\": isLight ? Ai360ModelLogo : Ai360ModelLogoDark,\n        aimass: isLight ? AimassModelLogo : AimassModelLogoDark,\n        codegeex: isLight ? CodegeexModelLogo : CodegeexModelLogoDark,\n        copilot: isLight ? CopilotModelLogo : CopilotModelLogoDark,\n        creative: isLight ? CopilotModelLogo : CopilotModelLogoDark,\n        balanced: isLight ? CopilotModelLogo : CopilotModelLogoDark,\n        precise: isLight ? CopilotModelLogo : CopilotModelLogoDark,\n        dalle: isLight ? DalleModelLogo : DalleModelLogoDark,\n        \"dall-e\": isLight ? DalleModelLogo : DalleModelLogoDark,\n        dbrx: isLight ? DbrxModelLogo : DbrxModelLogo,\n        flashaudio: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark,\n        flux: isLight ? FluxModelLogo : FluxModelLogoDark,\n        grok: isLight ? GrokModelLogo : GrokModelLogoDark,\n        hunyuan: isLight ? HunyuanModelLogo : HunyuanModelLogoDark,\n        internlm: isLight ? InternlmModelLogo : InternlmModelLogoDark,\n        internvl: InternvlModelLogo,\n        llava: isLight ? LLavaModelLogo : LLavaModelLogoDark,\n        magic: isLight ? MagicModelLogo : MagicModelLogoDark,\n        midjourney: isLight ? MidjourneyModelLogo : MidjourneyModelLogoDark,\n        \"mj-\": isLight ? MidjourneyModelLogo : MidjourneyModelLogoDark,\n        \"tao-\": isLight ? WenxinModelLogo : WenxinModelLogoDark,\n        \"ernie-\": isLight ? WenxinModelLogo : WenxinModelLogoDark,\n        voice: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark,\n        \"tts-1\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"whisper-\": isLight ? ChatGptModelLogo : ChatGptModelLogoDark,\n        \"stable-\": isLight ? StabilityModelLogo : StabilityModelLogoDark,\n        sd2: isLight ? StabilityModelLogo : StabilityModelLogoDark,\n        sd3: isLight ? StabilityModelLogo : StabilityModelLogoDark,\n        sdxl: isLight ? StabilityModelLogo : StabilityModelLogoDark,\n        sparkdesk: isLight ? SparkDeskModelLogo : SparkDeskModelLogoDark,\n        generalv: isLight ? SparkDeskModelLogo : SparkDeskModelLogoDark,\n        wizardlm: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,\n        microsoft: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,\n        hermes: isLight ? NousResearchModelLogo : NousResearchModelLogoDark,\n        gryphe: isLight ? GrypheModelLogo : GrypheModelLogoDark,\n        suno: isLight ? SunoModelLogo : SunoModelLogoDark,\n        chirp: isLight ? SunoModelLogo : SunoModelLogoDark,\n        luma: isLight ? LumaModelLogo : LumaModelLogoDark,\n        keling: isLight ? KeLingModelLogo : KeLingModelLogoDark,\n        \"vidu-\": isLight ? ViduModelLogo : ViduModelLogoDark,\n        ai21: isLight ? Ai21ModelLogo : Ai21ModelLogoDark,\n        \"jamba-\": isLight ? Ai21ModelLogo : Ai21ModelLogoDark,\n        mythomax: isLight ? GrypheModelLogo : GrypheModelLogoDark,\n        nvidia: isLight ? NvidiaModelLogo : NvidiaModelLogoDark,\n        dianxin: isLight ? DianxinModelLogo : DianxinModelLogoDark,\n        tele: isLight ? TeleModelLogo : TeleModelLogoDark,\n        adept: isLight ? AdeptModelLogo : AdeptModelLogoDark,\n        aisingapore: isLight ? AisingaporeModelLogo : AisingaporeModelLogoDark,\n        bigcode: isLight ? BigcodeModelLogo : BigcodeModelLogoDark,\n        mediatek: isLight ? MediatekModelLogo : MediatekModelLogoDark,\n        upstage: isLight ? UpstageModelLogo : UpstageModelLogoDark,\n        rakutenai: isLight ? RakutenaiModelLogo : RakutenaiModelLogoDark,\n        ibm: isLight ? IbmModelLogo : IbmModelLogoDark,\n        \"google/\": isLight ? GoogleModelLogo : GoogleModelLogoDark,\n        xirang: isLight ? XirangModelLogo : XirangModelLogoDark,\n        hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,\n        embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,\n        perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,\n        sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,\n        \"bge-\": BgeModelLogo,\n        \"voyage-\": VoyageModelLogo,\n    };\n\n    for (const key in logoMap) {\n        const regex = new RegExp(key, \"i\");\n        if (regex.test(modelId)) {\n            return logoMap[key];\n        }\n    }\n\n    return isLight ? ChatGptModelLogo : ChatGptModelLogoDark;\n}\n\nexport const SystemModels: Record<string, Partial<Model>[]> = {\n    aihubmix: [\n        {\n            id: \"gpt-4o\",\n            provider: \"aihubmix\",\n            name: \"GPT-4o\",\n            group: \"GPT-4o\",\n        },\n        {\n            id: \"claude-3-5-sonnet-latest\",\n            provider: \"aihubmix\",\n            name: \"Claude 3.5 Sonnet\",\n            group: \"Claude 3.5\",\n        },\n        {\n            id: \"gemini-2.0-flash-exp-search\",\n            provider: \"aihubmix\",\n            name: \"Gemini 2.0 Flash Exp Search\",\n            group: \"Gemini 2.0\",\n        },\n        {\n            id: \"deepseek-chat\",\n            provider: \"aihubmix\",\n            name: \"DeepSeek Chat\",\n            group: \"DeepSeek Chat\",\n        },\n        {\n            id: \"aihubmix-Llama-3-3-70B-Instruct\",\n            provider: \"aihubmix\",\n            name: \"Llama-3.3-70b\",\n            group: \"Llama 3.3\",\n        },\n        {\n            id: \"Qwen/QVQ-72B-Preview\",\n            provider: \"aihubmix\",\n            name: \"Qwen/QVQ-72B\",\n            group: \"Qwen\",\n        },\n    ],\n    o3: [\n        {\n            id: \"gpt-4o\",\n            provider: \"o3\",\n            name: \"GPT-4o\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"o1-mini\",\n            provider: \"o3\",\n            name: \"o1-mini\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"o1-preview\",\n            provider: \"o3\",\n            name: \"o1-preview\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"o3-mini\",\n            provider: \"o3\",\n            name: \"o3-mini\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"o3-mini-high\",\n            provider: \"o3\",\n            name: \"o3-mini-high\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"claude-3-7-sonnet-20250219\",\n            provider: \"o3\",\n            name: \"claude-3-7-sonnet-20250219\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"claude-3-5-sonnet-20241022\",\n            provider: \"o3\",\n            name: \"claude-3-5-sonnet-20241022\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"claude-3-5-haiku-20241022\",\n            provider: \"o3\",\n            name: \"claude-3-5-haiku-20241022\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"claude-3-opus-20240229\",\n            provider: \"o3\",\n            name: \"claude-3-opus-20240229\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"claude-3-haiku-20240307\",\n            provider: \"o3\",\n            name: \"claude-3-haiku-20240307\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"claude-3-5-sonnet-20240620\",\n            provider: \"o3\",\n            name: \"claude-3-5-sonnet-20240620\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"deepseek-ai/Deepseek-R1\",\n            provider: \"o3\",\n            name: \"DeepSeek R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-reasoner\",\n            provider: \"o3\",\n            name: \"deepseek-reasoner\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-chat\",\n            provider: \"o3\",\n            name: \"deepseek-chat\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-ai/DeepSeek-V3\",\n            provider: \"o3\",\n            name: \"DeepSeek V3\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"text-embedding-3-small\",\n            provider: \"o3\",\n            name: \"text-embedding-3-small\",\n            group: \"model.embedModels\",\n        },\n        {\n            id: \"text-embedding-ada-002\",\n            provider: \"o3\",\n            name: \"text-embedding-ada-002\",\n            group: \"model.embedModels\",\n        },\n        {\n            id: \"text-embedding-v2\",\n            provider: \"o3\",\n            name: \"text-embedding-v2\",\n            group: \"model.embedModels\",\n        },\n        {\n            id: \"Doubao-embedding\",\n            provider: \"o3\",\n            name: \"Doubao-embedding\",\n            group: \"model.embedModels\",\n        },\n        {\n            id: \"Doubao-embedding-large\",\n            provider: \"o3\",\n            name: \"Doubao-embedding-large\",\n            group: \"model.embedModels\",\n        },\n    ],\n    ollama: [],\n    lmstudio: [],\n    silicon: [\n        {\n            id: \"deepseek-ai/DeepSeek-R1\",\n            name: \"deepseek-ai/DeepSeek-R1\",\n            provider: \"silicon\",\n            group: \"deepseek-ai\",\n        },\n        {\n            id: \"deepseek-ai/DeepSeek-V3\",\n            name: \"deepseek-ai/DeepSeek-V3\",\n            provider: \"silicon\",\n            group: \"deepseek-ai\",\n        },\n        {\n            id: \"Qwen/Qwen2.5-7B-Instruct\",\n            provider: \"silicon\",\n            name: \"Qwen2.5-7B-Instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"meta-llama/Llama-3.3-70B-Instruct\",\n            name: \"meta-llama/Llama-3.3-70B-Instruct\",\n            provider: \"silicon\",\n            group: \"meta-llama\",\n        },\n        {\n            id: \"BAAI/bge-m3\",\n            name: \"BAAI/bge-m3\",\n            provider: \"silicon\",\n            group: \"BAAI\",\n        },\n    ],\n    ppio: [\n        {\n            id: \"deepseek/deepseek-r1/community\",\n            name: \"DeepSeek: DeepSeek R1 (Community)\",\n            provider: \"ppio\",\n            group: \"deepseek\",\n        },\n        {\n            id: \"deepseek/deepseek-v3/community\",\n            name: \"DeepSeek: DeepSeek V3 (Community)\",\n            provider: \"ppio\",\n            group: \"deepseek\",\n        },\n        {\n            id: \"deepseek/deepseek-r1\",\n            provider: \"ppio\",\n            name: \"DeepSeek R1\",\n            group: \"deepseek\",\n        },\n        {\n            id: \"deepseek/deepseek-v3\",\n            provider: \"ppio\",\n            name: \"DeepSeek V3\",\n            group: \"deepseek\",\n        },\n        {\n            id: \"qwen/qwen-2.5-72b-instruct\",\n            provider: \"ppio\",\n            name: \"Qwen2.5-72B-Instruct\",\n            group: \"qwen\",\n        },\n        {\n            id: \"qwen/qwen2.5-32b-instruct\",\n            provider: \"ppio\",\n            name: \"Qwen2.5-32B-Instruct\",\n            group: \"qwen\",\n        },\n        {\n            id: \"meta-llama/llama-3.1-70b-instruct\",\n            provider: \"ppio\",\n            name: \"Llama-3.1-70B-Instruct\",\n            group: \"meta-llama\",\n        },\n        {\n            id: \"meta-llama/llama-3.1-8b-instruct\",\n            provider: \"ppio\",\n            name: \"Llama-3.1-8B-Instruct\",\n            group: \"meta-llama\",\n        },\n        {\n            id: \"01-ai/yi-1.5-34b-chat\",\n            provider: \"ppio\",\n            name: \"Yi-1.5-34B-Chat\",\n            group: \"01-ai\",\n        },\n        {\n            id: \"01-ai/yi-1.5-9b-chat\",\n            provider: \"ppio\",\n            name: \"Yi-1.5-9B-Chat\",\n            group: \"01-ai\",\n        },\n    ],\n    alayanew: [],\n    openai: [\n        {\n            id: \"gpt-4.5-preview\",\n            provider: \"openai\",\n            name: \"gpt-4.5-preview\",\n            group: \"gpt-4.5\",\n        },\n        { id: \"gpt-4o\", provider: \"openai\", name: \"GPT-4o\", group: \"GPT 4o\" },\n        {\n            id: \"gpt-4o-mini\",\n            provider: \"openai\",\n            name: \"GPT-4o-mini\",\n            group: \"GPT 4o\",\n        },\n        { id: \"o1-mini\", provider: \"openai\", name: \"o1-mini\", group: \"o1\" },\n        {\n            id: \"o1-preview\",\n            provider: \"openai\",\n            name: \"o1-preview\",\n            group: \"o1\",\n        },\n    ],\n    \"azure-openai\": [\n        {\n            id: \"gpt-4o\",\n            provider: \"azure-openai\",\n            name: \"GPT-4o\",\n            group: \"GPT 4o\",\n        },\n        {\n            id: \"gpt-4o-mini\",\n            provider: \"azure-openai\",\n            name: \"GPT-4o-mini\",\n            group: \"GPT 4o\",\n        },\n    ],\n    gemini: [\n        {\n            id: \"gemini-1.5-flash\",\n            provider: \"gemini\",\n            name: \"Gemini 1.5 Flash\",\n            group: \"Gemini 1.5\",\n        },\n        {\n            id: \"gemini-1.5-flash-8b\",\n            provider: \"gemini\",\n            name: \"Gemini 1.5 Flash (8B)\",\n            group: \"Gemini 1.5\",\n        },\n        {\n            id: \"gemini-1.5-pro\",\n            name: \"Gemini 1.5 Pro\",\n            provider: \"gemini\",\n            group: \"Gemini 1.5\",\n        },\n        {\n            id: \"gemini-2.0-flash\",\n            provider: \"gemini\",\n            name: \"Gemini 2.0 Flash\",\n            group: \"Gemini 2.0\",\n        },\n    ],\n    anthropic: [\n        {\n            id: \"claude-3-7-sonnet-20250219\",\n            provider: \"anthropic\",\n            name: \"Claude 3.7 Sonnet\",\n            group: \"Claude 3.7\",\n        },\n        {\n            id: \"claude-3-5-sonnet-20241022\",\n            provider: \"anthropic\",\n            name: \"Claude 3.5 Sonnet\",\n            group: \"Claude 3.5\",\n        },\n        {\n            id: \"claude-3-5-haiku-20241022\",\n            provider: \"anthropic\",\n            name: \"Claude 3.5 Haiku\",\n            group: \"Claude 3.5\",\n        },\n        {\n            id: \"claude-3-5-sonnet-20240620\",\n            provider: \"anthropic\",\n            name: \"Claude 3.5 Sonnet (Legacy)\",\n            group: \"Claude 3.5\",\n        },\n        {\n            id: \"claude-3-opus-20240229\",\n            provider: \"anthropic\",\n            name: \"Claude 3 Opus\",\n            group: \"Claude 3\",\n        },\n        {\n            id: \"claude-3-haiku-20240307\",\n            provider: \"anthropic\",\n            name: \"Claude 3 Haiku\",\n            group: \"Claude 3\",\n        },\n    ],\n    \"gitee-ai\": [\n        {\n            id: \"DeepSeek-R1-Distill-Qwen-32B\",\n            name: \"DeepSeek-R1-Distill-Qwen-32B\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"DeepSeek-R1-Distill-Qwen-1.5B\",\n            name: \"DeepSeek-R1-Distill-Qwen-1.5B\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"DeepSeek-R1-Distill-Qwen-14B\",\n            name: \"DeepSeek-R1-Distill-Qwen-14B\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"DeepSeek-R1-Distill-Qwen-7B\",\n            name: \"DeepSeek-R1-Distill-Qwen-7B\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"DeepSeek-V3\",\n            name: \"DeepSeek-V3\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"DeepSeek-R1\",\n            name: \"DeepSeek-R1\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-coder-33B-instruct\",\n            name: \"deepseek-coder-33B-instruct\",\n            provider: \"gitee-ai\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"Qwen2.5-72B-Instruct\",\n            name: \"Qwen2.5-72B-Instruct\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen2.5-14B-Instruct\",\n            name: \"Qwen2.5-14B-Instruct\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen2-7B-Instruct\",\n            name: \"Qwen2-7B-Instruct\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen2.5-32B-Instruct\",\n            name: \"Qwen2.5-32B-Instruct\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen2-72B-Instruct\",\n            name: \"Qwen2-72B-Instruct\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen2-VL-72B\",\n            name: \"Qwen2-VL-72B\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"QwQ-32B-Preview\",\n            name: \"QwQ-32B-Preview\",\n            provider: \"gitee-ai\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Yi-34B-Chat\",\n            name: \"Yi-34B-Chat\",\n            provider: \"gitee-ai\",\n            group: \"01-ai\",\n        },\n        {\n            id: \"glm-4-9b-chat\",\n            name: \"glm-4-9b-chat\",\n            provider: \"gitee-ai\",\n            group: \"THUDM\",\n        },\n        {\n            id: \"codegeex4-all-9b\",\n            name: \"codegeex4-all-9b\",\n            provider: \"gitee-ai\",\n            group: \"THUDM\",\n        },\n        {\n            id: \"InternVL2-8B\",\n            name: \"InternVL2-8B\",\n            provider: \"gitee-ai\",\n            group: \"OpenGVLab\",\n        },\n        {\n            id: \"InternVL2.5-26B\",\n            name: \"InternVL2.5-26B\",\n            provider: \"gitee-ai\",\n            group: \"OpenGVLab\",\n        },\n        {\n            id: \"InternVL2.5-78B\",\n            name: \"InternVL2.5-78B\",\n            provider: \"gitee-ai\",\n            group: \"OpenGVLab\",\n        },\n        {\n            id: \"bge-large-zh-v1.5\",\n            name: \"bge-large-zh-v1.5\",\n            provider: \"gitee-ai\",\n            group: \"BAAI\",\n        },\n        {\n            id: \"bge-small-zh-v1.5\",\n            name: \"bge-small-zh-v1.5\",\n            provider: \"gitee-ai\",\n            group: \"BAAI\",\n        },\n        {\n            id: \"bge-m3\",\n            name: \"bge-m3\",\n            provider: \"gitee-ai\",\n            group: \"BAAI\",\n        },\n        {\n            id: \"bce-embedding-base_v1\",\n            name: \"bce-embedding-base_v1\",\n            provider: \"gitee-ai\",\n            group: \"netease-youdao\",\n        },\n    ],\n    deepseek: [\n        {\n            id: \"deepseek-chat\",\n            provider: \"deepseek\",\n            name: \"DeepSeek Chat\",\n            group: \"DeepSeek Chat\",\n        },\n        {\n            id: \"deepseek-reasoner\",\n            provider: \"deepseek\",\n            name: \"DeepSeek Reasoner\",\n            group: \"DeepSeek Reasoner\",\n        },\n    ],\n    together: [\n        {\n            id: \"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo\",\n            provider: \"together\",\n            name: \"Llama-3.2-11B-Vision\",\n            group: \"Llama-3.2\",\n        },\n        {\n            id: \"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo\",\n            provider: \"together\",\n            name: \"Llama-3.2-90B-Vision\",\n            group: \"Llama-3.2\",\n        },\n        {\n            id: \"google/gemma-2-27b-it\",\n            provider: \"together\",\n            name: \"gemma-2-27b-it\",\n            group: \"Gemma\",\n        },\n        {\n            id: \"google/gemma-2-9b-it\",\n            provider: \"together\",\n            name: \"gemma-2-9b-it\",\n            group: \"Gemma\",\n        },\n    ],\n    ocoolai: [\n        {\n            id: \"deepseek-chat\",\n            provider: \"ocoolai\",\n            name: \"deepseek-chat\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-reasoner\",\n            provider: \"ocoolai\",\n            name: \"deepseek-reasoner\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-ai/DeepSeek-R1\",\n            provider: \"ocoolai\",\n            name: \"deepseek-ai/DeepSeek-R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"HiSpeed/DeepSeek-R1\",\n            provider: \"ocoolai\",\n            name: \"HiSpeed/DeepSeek-R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"ocoolAI/DeepSeek-R1\",\n            provider: \"ocoolai\",\n            name: \"ocoolAI/DeepSeek-R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"Azure/DeepSeek-R1\",\n            provider: \"ocoolai\",\n            name: \"Azure/DeepSeek-R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"gpt-4o\",\n            provider: \"ocoolai\",\n            name: \"gpt-4o\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"gpt-4o-all\",\n            provider: \"ocoolai\",\n            name: \"gpt-4o-all\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"gpt-4o-mini\",\n            provider: \"ocoolai\",\n            name: \"gpt-4o-mini\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"gpt-4\",\n            provider: \"ocoolai\",\n            name: \"gpt-4\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"o1-preview\",\n            provider: \"ocoolai\",\n            name: \"o1-preview\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"o1-mini\",\n            provider: \"ocoolai\",\n            name: \"o1-mini\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"claude-3-5-sonnet-20240620\",\n            provider: \"ocoolai\",\n            name: \"claude-3-5-sonnet-20240620\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"claude-3-5-haiku-20241022\",\n            provider: \"ocoolai\",\n            name: \"claude-3-5-haiku-20241022\",\n            group: \"Anthropic\",\n        },\n        {\n            id: \"gemini-pro\",\n            provider: \"ocoolai\",\n            name: \"gemini-pro\",\n            group: \"Gemini\",\n        },\n        {\n            id: \"gemini-1.5-pro\",\n            provider: \"ocoolai\",\n            name: \"gemini-1.5-pro\",\n            group: \"Gemini\",\n        },\n        {\n            id: \"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo\",\n            provider: \"ocoolai\",\n            name: \"Llama-3.2-90B-Vision-Instruct-Turbo\",\n            group: \"Llama-3.2\",\n        },\n        {\n            id: \"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo\",\n            provider: \"ocoolai\",\n            name: \"Llama-3.2-11B-Vision-Instruct-Turbo\",\n            group: \"Llama-3.2\",\n        },\n        {\n            id: \"meta-llama/Llama-3.2-3B-Vision-Instruct-Turbo\",\n            provider: \"ocoolai\",\n            name: \"Llama-3.2-3B-Vision-Instruct-Turbo\",\n            group: \"Llama-3.2\",\n        },\n        {\n            id: \"google/gemma-2-27b-it\",\n            provider: \"ocoolai\",\n            name: \"gemma-2-27b-it\",\n            group: \"Gemma\",\n        },\n        {\n            id: \"google/gemma-2-9b-it\",\n            provider: \"ocoolai\",\n            name: \"gemma-2-9b-it\",\n            group: \"Gemma\",\n        },\n        {\n            id: \"Doubao-embedding\",\n            provider: \"ocoolai\",\n            name: \"Doubao-embedding\",\n            group: \"Doubao\",\n        },\n        {\n            id: \"text-embedding-3-large\",\n            provider: \"ocoolai\",\n            name: \"text-embedding-3-large\",\n            group: \"Embedding\",\n        },\n        {\n            id: \"text-embedding-3-small\",\n            provider: \"ocoolai\",\n            name: \"text-embedding-3-small\",\n            group: \"Embedding\",\n        },\n        {\n            id: \"text-embedding-v2\",\n            provider: \"ocoolai\",\n            name: \"text-embedding-v2\",\n            group: \"Embedding\",\n        },\n    ],\n    github: [\n        {\n            id: \"gpt-4o\",\n            provider: \"github\",\n            name: \"OpenAI GPT-4o\",\n            group: \"OpenAI\",\n        },\n    ],\n    copilot: [\n        {\n            id: \"gpt-4o-mini\",\n            provider: \"copilot\",\n            name: \"OpenAI GPT-4o-mini\",\n            group: \"OpenAI\",\n        },\n    ],\n    yi: [\n        {\n            id: \"yi-lightning\",\n            name: \"Yi Lightning\",\n            provider: \"yi\",\n            group: \"yi-lightning\",\n        },\n        {\n            id: \"yi-vision-v2\",\n            name: \"Yi Vision v2\",\n            provider: \"yi\",\n            group: \"yi-vision\",\n        },\n    ],\n    zhipu: [\n        {\n            id: \"glm-zero-preview\",\n            provider: \"zhipu\",\n            name: \"GLM-Zero-Preview\",\n            group: \"GLM-Zero\",\n        },\n        {\n            id: \"glm-4-0520\",\n            provider: \"zhipu\",\n            name: \"GLM-4-0520\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4-long\",\n            provider: \"zhipu\",\n            name: \"GLM-4-Long\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4-plus\",\n            provider: \"zhipu\",\n            name: \"GLM-4-Plus\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4-air\",\n            provider: \"zhipu\",\n            name: \"GLM-4-Air\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4-airx\",\n            provider: \"zhipu\",\n            name: \"GLM-4-AirX\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4-flash\",\n            provider: \"zhipu\",\n            name: \"GLM-4-Flash\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4-flashx\",\n            provider: \"zhipu\",\n            name: \"GLM-4-FlashX\",\n            group: \"GLM-4\",\n        },\n        {\n            id: \"glm-4v\",\n            provider: \"zhipu\",\n            name: \"GLM 4V\",\n            group: \"GLM-4v\",\n        },\n        {\n            id: \"glm-4v-flash\",\n            provider: \"zhipu\",\n            name: \"GLM-4V-Flash\",\n            group: \"GLM-4v\",\n        },\n        {\n            id: \"glm-4v-plus\",\n            provider: \"zhipu\",\n            name: \"GLM-4V-Plus\",\n            group: \"GLM-4v\",\n        },\n        {\n            id: \"glm-4-alltools\",\n            provider: \"zhipu\",\n            name: \"GLM-4-AllTools\",\n            group: \"GLM-4-AllTools\",\n        },\n        {\n            id: \"embedding-3\",\n            provider: \"zhipu\",\n            name: \"Embedding-3\",\n            group: \"Embedding\",\n        },\n    ],\n    moonshot: [\n        {\n            id: \"moonshot-v1-auto\",\n            name: \"moonshot-v1-auto\",\n            provider: \"moonshot\",\n            group: \"moonshot-v1\",\n        },\n    ],\n    baichuan: [\n        {\n            id: \"Baichuan4\",\n            provider: \"baichuan\",\n            name: \"Baichuan4\",\n            group: \"Baichuan4\",\n        },\n        {\n            id: \"Baichuan3-Turbo\",\n            provider: \"baichuan\",\n            name: \"Baichuan3 Turbo\",\n            group: \"Baichuan3\",\n        },\n        {\n            id: \"Baichuan3-Turbo-128k\",\n            provider: \"baichuan\",\n            name: \"Baichuan3 Turbo 128k\",\n            group: \"Baichuan3\",\n        },\n    ],\n    modelscope: [\n        {\n            id: \"Qwen/Qwen2.5-72B-Instruct\",\n            name: \"Qwen/Qwen2.5-72B-Instruct\",\n            provider: \"modelscope\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen/Qwen2.5-VL-72B-Instruct\",\n            name: \"Qwen/Qwen2.5-VL-72B-Instruct\",\n            provider: \"modelscope\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"Qwen/Qwen2.5-Coder-32B-Instruct\",\n            name: \"Qwen/Qwen2.5-Coder-32B-Instruct\",\n            provider: \"modelscope\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"deepseek-ai/DeepSeek-R1\",\n            name: \"deepseek-ai/DeepSeek-R1\",\n            provider: \"modelscope\",\n            group: \"deepseek-ai\",\n        },\n        {\n            id: \"deepseek-ai/DeepSeek-V3\",\n            name: \"deepseek-ai/DeepSeek-V3\",\n            provider: \"modelscope\",\n            group: \"deepseek-ai\",\n        },\n    ],\n    bailian: [\n        {\n            id: \"qwen-vl-plus\",\n            name: \"qwen-vl-plus\",\n            provider: \"dashscope\",\n            group: \"qwen-vl\",\n        },\n        {\n            id: \"qwen-coder-plus\",\n            name: \"qwen-coder-plus\",\n            provider: \"dashscope\",\n            group: \"qwen-coder\",\n        },\n        {\n            id: \"qwen-turbo\",\n            name: \"qwen-turbo\",\n            provider: \"dashscope\",\n            group: \"qwen-turbo\",\n        },\n        {\n            id: \"qwen-plus\",\n            name: \"qwen-plus\",\n            provider: \"dashscope\",\n            group: \"qwen-plus\",\n        },\n        {\n            id: \"qwen-max\",\n            name: \"qwen-max\",\n            provider: \"dashscope\",\n            group: \"qwen-max\",\n        },\n    ],\n    stepfun: [\n        {\n            id: \"step-1-8k\",\n            provider: \"stepfun\",\n            name: \"Step 1 8K\",\n            group: \"Step 1\",\n        },\n        {\n            id: \"step-1-flash\",\n            provider: \"stepfun\",\n            name: \"Step 1 Flash\",\n            group: \"Step 1\",\n        },\n    ],\n    doubao: [],\n    minimax: [\n        {\n            id: \"abab6.5s-chat\",\n            provider: \"minimax\",\n            name: \"abab6.5s\",\n            group: \"abab6\",\n        },\n        {\n            id: \"abab6.5g-chat\",\n            provider: \"minimax\",\n            name: \"abab6.5g\",\n            group: \"abab6\",\n        },\n        {\n            id: \"abab6.5t-chat\",\n            provider: \"minimax\",\n            name: \"abab6.5t\",\n            group: \"abab6\",\n        },\n        {\n            id: \"abab5.5s-chat\",\n            provider: \"minimax\",\n            name: \"abab5.5s\",\n            group: \"abab5\",\n        },\n        {\n            id: \"minimax-text-01\",\n            provider: \"minimax\",\n            name: \"minimax-01\",\n            group: \"minimax-01\",\n        },\n    ],\n    hyperbolic: [\n        {\n            id: \"Qwen/Qwen2-VL-72B-Instruct\",\n            provider: \"hyperbolic\",\n            name: \"Qwen2-VL-72B-Instruct\",\n            group: \"Qwen2-VL\",\n        },\n        {\n            id: \"Qwen/Qwen2-VL-7B-Instruct\",\n            provider: \"hyperbolic\",\n            name: \"Qwen2-VL-7B-Instruct\",\n            group: \"Qwen2-VL\",\n        },\n        {\n            id: \"mistralai/Pixtral-12B-2409\",\n            provider: \"hyperbolic\",\n            name: \"Pixtral-12B-2409\",\n            group: \"Pixtral\",\n        },\n        {\n            id: \"meta-llama/Meta-Llama-3.1-405B\",\n            provider: \"hyperbolic\",\n            name: \"Meta-Llama-3.1-405B\",\n            group: \"Meta-Llama-3.1\",\n        },\n    ],\n    grok: [\n        {\n            id: \"grok-beta\",\n            provider: \"grok\",\n            name: \"Grok Beta\",\n            group: \"Grok\",\n        },\n        {\n            id: \"grok-vision-beta\",\n            provider: \"grok\",\n            name: \"Grok Vision Beta\",\n            group: \"Grok\",\n        },\n    ],\n    mistral: [\n        {\n            id: \"pixtral-12b-2409\",\n            provider: \"mistral\",\n            name: \"Pixtral 12B [Free]\",\n            group: \"Pixtral\",\n        },\n        {\n            id: \"pixtral-large-latest\",\n            provider: \"mistral\",\n            name: \"Pixtral Large\",\n            group: \"Pixtral\",\n        },\n        {\n            id: \"ministral-3b-latest\",\n            provider: \"mistral\",\n            name: \"Mistral 3B [Free]\",\n            group: \"Mistral Mini\",\n        },\n        {\n            id: \"ministral-8b-latest\",\n            provider: \"mistral\",\n            name: \"Mistral 8B [Free]\",\n            group: \"Mistral Mini\",\n        },\n        {\n            id: \"codestral-latest\",\n            provider: \"mistral\",\n            name: \"Mistral Codestral\",\n            group: \"Mistral Code\",\n        },\n        {\n            id: \"mistral-large-latest\",\n            provider: \"mistral\",\n            name: \"Mistral Large\",\n            group: \"Mistral Chat\",\n        },\n        {\n            id: \"mistral-small-latest\",\n            provider: \"mistral\",\n            name: \"Mistral Small\",\n            group: \"Mistral Chat\",\n        },\n        {\n            id: \"open-mistral-nemo\",\n            provider: \"mistral\",\n            name: \"Mistral Nemo\",\n            group: \"Mistral Chat\",\n        },\n        {\n            id: \"mistral-embed\",\n            provider: \"mistral\",\n            name: \"Mistral Embedding\",\n            group: \"Mistral Embed\",\n        },\n    ],\n    jina: [\n        {\n            id: \"jina-clip-v1\",\n            provider: \"jina\",\n            name: \"jina-clip-v1\",\n            group: \"Jina Clip\",\n        },\n        {\n            id: \"jina-clip-v2\",\n            provider: \"jina\",\n            name: \"jina-clip-v2\",\n            group: \"Jina Clip\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-en\",\n            provider: \"jina\",\n            name: \"jina-embeddings-v2-base-en\",\n            group: \"Jina Embeddings V2\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-es\",\n            provider: \"jina\",\n            name: \"jina-embeddings-v2-base-es\",\n            group: \"Jina Embeddings V2\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-de\",\n            provider: \"jina\",\n            name: \"jina-embeddings-v2-base-de\",\n            group: \"Jina Embeddings V2\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-zh\",\n            provider: \"jina\",\n            name: \"jina-embeddings-v2-base-zh\",\n            group: \"Jina Embeddings V2\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-code\",\n            provider: \"jina\",\n            name: \"jina-embeddings-v2-base-code\",\n            group: \"Jina Embeddings V2\",\n        },\n        {\n            id: \"jina-embeddings-v3\",\n            provider: \"jina\",\n            name: \"jina-embeddings-v3\",\n            group: \"Jina Embeddings V3\",\n        },\n    ],\n    fireworks: [\n        {\n            id: \"accounts/fireworks/models/mythomax-l2-13b\",\n            provider: \"fireworks\",\n            name: \"mythomax-l2-13b\",\n            group: \"Gryphe\",\n        },\n        {\n            id: \"accounts/fireworks/models/llama-v3-70b-instruct\",\n            provider: \"fireworks\",\n            name: \"Llama-3-70B-Instruct\",\n            group: \"Llama3\",\n        },\n    ],\n    zhinao: [\n        {\n            id: \"360gpt-pro\",\n            provider: \"zhinao\",\n            name: \"360gpt-pro\",\n            group: \"360Gpt\",\n        },\n        {\n            id: \"360gpt-turbo\",\n            provider: \"zhinao\",\n            name: \"360gpt-turbo\",\n            group: \"360Gpt\",\n        },\n    ],\n    hunyuan: [\n        {\n            id: \"hunyuan-pro\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-pro\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-standard\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-standard\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-lite\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-lite\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-standard-256k\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-standard-256k\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-vision\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-vision\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-code\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-code\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-role\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-role\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-turbo\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-turbo\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-turbos-latest\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-turbos-latest\",\n            group: \"Hunyuan\",\n        },\n        {\n            id: \"hunyuan-embedding\",\n            provider: \"hunyuan\",\n            name: \"hunyuan-embedding\",\n            group: \"Embedding\",\n        },\n    ],\n    nvidia: [\n        {\n            id: \"01-ai/yi-large\",\n            provider: \"nvidia\",\n            name: \"yi-large\",\n            group: \"Yi\",\n        },\n        {\n            id: \"meta/llama-3.1-405b-instruct\",\n            provider: \"nvidia\",\n            name: \"llama-3.1-405b-instruct\",\n            group: \"llama-3.1\",\n        },\n    ],\n    openrouter: [\n        {\n            id: \"google/gemma-2-9b-it:free\",\n            provider: \"openrouter\",\n            name: \"Google: Gemma 2 9B\",\n            group: \"Gemma\",\n        },\n        {\n            id: \"microsoft/phi-3-mini-128k-instruct:free\",\n            provider: \"openrouter\",\n            name: \"Phi-3 Mini 128K Instruct\",\n            group: \"Phi\",\n        },\n        {\n            id: \"microsoft/phi-3-medium-128k-instruct:free\",\n            provider: \"openrouter\",\n            name: \"Phi-3 Medium 128K Instruct\",\n            group: \"Phi\",\n        },\n        {\n            id: \"meta-llama/llama-3-8b-instruct:free\",\n            provider: \"openrouter\",\n            name: \"Meta: Llama 3 8B Instruct\",\n            group: \"Llama3\",\n        },\n        {\n            id: \"mistralai/mistral-7b-instruct:free\",\n            provider: \"openrouter\",\n            name: \"Mistral: Mistral 7B Instruct\",\n            group: \"Mistral\",\n        },\n    ],\n    groq: [\n        {\n            id: \"llama3-8b-8192\",\n            provider: \"groq\",\n            name: \"LLaMA3 8B\",\n            group: \"Llama3\",\n        },\n        {\n            id: \"llama3-70b-8192\",\n            provider: \"groq\",\n            name: \"LLaMA3 70B\",\n            group: \"Llama3\",\n        },\n        {\n            id: \"mistral-saba-24b\",\n            provider: \"groq\",\n            name: \"Mistral Saba 24B\",\n            group: \"Mistral\",\n        },\n        {\n            id: \"gemma-9b-it\",\n            provider: \"groq\",\n            name: \"Gemma 9B\",\n            group: \"Gemma\",\n        },\n    ],\n    \"baidu-cloud\": [\n        {\n            id: \"deepseek-r1\",\n            provider: \"baidu-cloud\",\n            name: \"DeepSeek R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-v3\",\n            provider: \"baidu-cloud\",\n            name: \"DeepSeek V3\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"ernie-4.0-8k-latest\",\n            provider: \"baidu-cloud\",\n            name: \"ERNIE-4.0\",\n            group: \"ERNIE\",\n        },\n        {\n            id: \"ernie-4.0-turbo-8k-latest\",\n            provider: \"baidu-cloud\",\n            name: \"ERNIE 4.0 Trubo\",\n            group: \"ERNIE\",\n        },\n        {\n            id: \"ernie-speed-8k\",\n            provider: \"baidu-cloud\",\n            name: \"ERNIE Speed\",\n            group: \"ERNIE\",\n        },\n        {\n            id: \"ernie-lite-8k\",\n            provider: \"baidu-cloud\",\n            name: \"ERNIE Lite\",\n            group: \"ERNIE\",\n        },\n        {\n            id: \"bge-large-zh\",\n            provider: \"baidu-cloud\",\n            name: \"BGE Large ZH\",\n            group: \"Embedding\",\n        },\n        {\n            id: \"bge-large-en\",\n            provider: \"baidu-cloud\",\n            name: \"BGE Large EN\",\n            group: \"Embedding\",\n        },\n    ],\n    dmxapi: [\n        {\n            id: \"Qwen/Qwen2.5-7B-Instruct\",\n            provider: \"dmxapi\",\n            name: \"Qwen/Qwen2.5-7B-Instruct\",\n            group: \"model.freeModels\",\n        },\n        {\n            id: \"ERNIE-Speed-128K\",\n            provider: \"dmxapi\",\n            name: \"ERNIE-Speed-128K\",\n            group: \"model.freeModels\",\n        },\n        {\n            id: \"THUDM/glm-4-9b-chat\",\n            provider: \"dmxapi\",\n            name: \"THUDM/glm-4-9b-chat\",\n            group: \"model.freeModels\",\n        },\n        {\n            id: \"glm-4-flash\",\n            provider: \"dmxapi\",\n            name: \"glm-4-flash\",\n            group: \"model.freeModels\",\n        },\n        {\n            id: \"hunyuan-lite\",\n            provider: \"dmxapi\",\n            name: \"hunyuan-lite\",\n            group: \"model.freeModels\",\n        },\n        {\n            id: \"gpt-4o\",\n            provider: \"dmxapi\",\n            name: \"gpt-4o\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"gpt-4o-mini\",\n            provider: \"dmxapi\",\n            name: \"gpt-4o-mini\",\n            group: \"OpenAI\",\n        },\n        {\n            id: \"DMXAPI-DeepSeek-R1\",\n            provider: \"dmxapi\",\n            name: \"DMXAPI-DeepSeek-R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"DMXAPI-DeepSeek-V3\",\n            provider: \"dmxapi\",\n            name: \"DMXAPI-DeepSeek-V3\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"claude-3-5-sonnet-20241022\",\n            provider: \"dmxapi\",\n            name: \"claude-3-5-sonnet-20241022\",\n            group: \"Claude\",\n        },\n        {\n            id: \"gemini-2.0-flash\",\n            provider: \"dmxapi\",\n            name: \"gemini-2.0-flash\",\n            group: \"Gemini\",\n        },\n    ],\n    perplexity: [\n        {\n            id: \"sonar-reasoning-pro\",\n            provider: \"perplexity\",\n            name: \"sonar-reasoning-pro\",\n            group: \"Sonar\",\n        },\n        {\n            id: \"sonar-reasoning\",\n            provider: \"perplexity\",\n            name: \"sonar-reasoning\",\n            group: \"Sonar\",\n        },\n        {\n            id: \"sonar-pro\",\n            provider: \"perplexity\",\n            name: \"sonar-pro\",\n            group: \"Sonar\",\n        },\n        {\n            id: \"sonar\",\n            provider: \"perplexity\",\n            name: \"sonar\",\n            group: \"Sonar\",\n        },\n    ],\n    infini: [\n        {\n            id: \"deepseek-r1\",\n            provider: \"infini\",\n            name: \"deepseek-r1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-r1-distill-qwen-32b\",\n            provider: \"infini\",\n            name: \"deepseek-r1-distill-qwen-32b\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-v3\",\n            provider: \"infini\",\n            name: \"deepseek-v3\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"qwen2.5-72b-instruct\",\n            provider: \"infini\",\n            name: \"qwen2.5-72b-instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"qwen2.5-32b-instruct\",\n            provider: \"infini\",\n            name: \"qwen2.5-32b-instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"qwen2.5-14b-instruct\",\n            provider: \"infini\",\n            name: \"qwen2.5-14b-instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"qwen2.5-7b-instruct\",\n            provider: \"infini\",\n            name: \"qwen2.5-7b-instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"qwen2-72b-instruct\",\n            provider: \"infini\",\n            name: \"qwen2-72b-instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"qwq-32b-preview\",\n            provider: \"infini\",\n            name: \"qwq-32b-preview\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"qwen2.5-coder-32b-instruct\",\n            provider: \"infini\",\n            name: \"qwen2.5-coder-32b-instruct\",\n            group: \"Qwen\",\n        },\n        {\n            id: \"llama-3.3-70b-instruct\",\n            provider: \"infini\",\n            name: \"llama-3.3-70b-instruct\",\n            group: \"Llama\",\n        },\n        {\n            id: \"bge-m3\",\n            provider: \"infini\",\n            name: \"bge-m3\",\n            group: \"BAAI\",\n        },\n        {\n            id: \"gemma-2-27b-it\",\n            provider: \"infini\",\n            name: \"gemma-2-27b-it\",\n            group: \"Gemma\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-zh\",\n            provider: \"infini\",\n            name: \"jina-embeddings-v2-base-zh\",\n            group: \"Jina\",\n        },\n        {\n            id: \"jina-embeddings-v2-base-code\",\n            provider: \"infini\",\n            name: \"jina-embeddings-v2-base-code\",\n            group: \"Jina\",\n        },\n    ],\n    xirang: [],\n    \"tencent-cloud-ti\": [\n        {\n            id: \"deepseek-r1\",\n            provider: \"tencent-cloud-ti\",\n            name: \"DeepSeek R1\",\n            group: \"DeepSeek\",\n        },\n        {\n            id: \"deepseek-v3\",\n            provider: \"tencent-cloud-ti\",\n            name: \"DeepSeek V3\",\n            group: \"DeepSeek\",\n        },\n    ],\n    gpustack: [],\n    voyageai: [\n        {\n            id: \"voyage-3-large\",\n            provider: \"voyageai\",\n            name: \"voyage-3-large\",\n            group: \"Voyage Embeddings V3\",\n        },\n        {\n            id: \"voyage-3\",\n            provider: \"voyageai\",\n            name: \"voyage-3\",\n            group: \"Voyage Embeddings V3\",\n        },\n        {\n            id: \"voyage-3-lite\",\n            provider: \"voyageai\",\n            name: \"voyage-3-lite\",\n            group: \"Voyage Embeddings V3\",\n        },\n        {\n            id: \"voyage-code-3\",\n            provider: \"voyageai\",\n            name: \"voyage-code-3\",\n            group: \"Voyage Embeddings V3\",\n        },\n        {\n            id: \"voyage-finance-3\",\n            provider: \"voyageai\",\n            name: \"voyage-finance-3\",\n            group: \"Voyage Embeddings V2\",\n        },\n        {\n            id: \"voyage-law-2\",\n            provider: \"voyageai\",\n            name: \"voyage-law-2\",\n            group: \"Voyage Embeddings V2\",\n        },\n        {\n            id: \"voyage-code-2\",\n            provider: \"voyageai\",\n            name: \"voyage-code-2\",\n            group: \"Voyage Embeddings V2\",\n        },\n        {\n            id: \"rerank-2\",\n            provider: \"voyageai\",\n            name: \"rerank-2\",\n            group: \"Voyage Rerank V2\",\n        },\n        {\n            id: \"rerank-2-lite\",\n            provider: \"voyageai\",\n            name: \"rerank-2-lite\",\n            group: \"Voyage Rerank V2\",\n        },\n    ],\n};\n"
  },
  {
    "path": "src/module/Model/provider/driver/base.ts",
    "content": "import { ChatParam, ProviderType } from \"../../types\";\nimport { ModelChatResult } from \"../provider\";\n\nexport class AbstractModelProvider {\n    config: {\n        type: ProviderType;\n        url: string;\n        apiUrl: string;\n        apiHost: string;\n        apiKey: string;\n        [key: string]: any;\n    };\n\n    constructor(config: {\n        type: ProviderType;\n        url: string;\n        apiUrl: string;\n        apiHost: string;\n        apiKey: string;\n        [key: string]: any;\n    }) {\n        this.config = config;\n    }\n\n    async chat(prompt: string, chatParam: ChatParam): Promise<ModelChatResult> {\n        return Promise.reject(new Error(\"Method not implemented.\"));\n    }\n}\n"
  },
  {
    "path": "src/module/Model/provider/driver/openai.ts",
    "content": "import { ModelChatResult } from \"../provider\";\nimport { ChatParam, ProviderType } from \"../../types\";\nimport { AbstractModelProvider } from \"./base\";\n\nexport class OpenAiModelProvider extends AbstractModelProvider {\n    constructor(config: {\n        type: ProviderType;\n        url: string;\n        apiUrl: string;\n        apiHost: string;\n        apiKey: string;\n        [p: string]: any;\n    }) {\n        super(config);\n    }\n\n    async chat(prompt: string, chatParam: ChatParam): Promise<ModelChatResult> {\n        // this.config.url =  'http://localhost:3000/v1/chat/completions';\n        // this.config.apiKey = '';\n        chatParam = Object.assign(\n            {\n                systemPrompt: null,\n            },\n            chatParam,\n        );\n        const messages: any[] = [];\n        if (chatParam.systemPrompt) {\n            messages.push({ role: \"system\", content: chatParam.systemPrompt });\n        }\n        messages.push({ role: \"user\", content: prompt });\n        const response = await fetch(this.config.url, {\n            method: \"POST\",\n            headers: {\n                Authorization: `Bearer ${this.config.apiKey}`,\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                model: this.config.modelId,\n                messages: messages,\n            }),\n        });\n        if (!response.ok) {\n            const error = await response.text();\n            throw `Request failed: ${response.status}\\n${error}`;\n        }\n        // check if is json\n        if (\n            !response.headers.get(\"content-type\")?.includes(\"application/json\")\n        ) {\n            const error = await response.text();\n            throw `Response is not json: ${response.status}\\n${error}`;\n        }\n        const data = await response.json();\n        try {\n            const content = data.choices[0].message.content;\n            return {\n                code: 0,\n                msg: \"ok\",\n                data: {\n                    content,\n                },\n            };\n        } catch (e) {\n            throw `Invalid response format: ${JSON.stringify(data)}`;\n        }\n    }\n}\n"
  },
  {
    "path": "src/module/Model/provider/provider.ts",
    "content": "import { ChatParam, ProviderType } from \"../types\";\nimport { OpenAiModelProvider } from \"./driver/openai\";\nimport { mapError } from \"../../../lib/error\";\n\nconst ModelProviderMap = {\n    openai: OpenAiModelProvider,\n};\n\nexport type ModelChatResult = {\n    code: number;\n    msg: string;\n    data?: {\n        content?: string;\n        [key: string]: any;\n    };\n};\n\nexport const ModelProvider = {\n    apiUrl(type: ProviderType, apiUrl: string, apiHost: string = \"\") {\n        let url = apiUrl;\n        if (apiHost) {\n            url = apiHost;\n        }\n        // console.log('ModelProvider.apiUrl', {type, apiUrl, apiHost, url});\n        switch (type) {\n            case \"openai\":\n                /**\n                 * 根据传入的 url 判断是否需要在其末尾加 `/v[数字]/`。\n                 * - 如果 以 `/` 结尾，则不加\n                 * - 要加：其余情况。\n                 */\n                if (url.endsWith(\"/\")) {\n                    return `${url}chat/completions`;\n                }\n                if (url.endsWith(\"/chat/completions\")) {\n                    return url;\n                }\n                return `${url}/v1/chat/completions`;\n        }\n        throw new Error(`Unsupported provider type: ${type}`);\n    },\n    async chat(\n        prompt: string,\n        chatParam: ChatParam,\n        config: {\n            type: ProviderType;\n            modelId: string;\n            apiUrl: string;\n            apiHost: string;\n            apiKey: string;\n        },\n    ): Promise<ModelChatResult> {\n        let url = this.apiUrl(config.type, config.apiUrl, config.apiHost);\n        if (!(config.type in ModelProviderMap)) {\n            return {\n                code: -1,\n                msg: `Unsupported provider type: ${config.type}`,\n            };\n        }\n        const provider = new ModelProviderMap[config.type]({\n            ...config,\n            url,\n        });\n        try {\n            return provider.chat(prompt, chatParam);\n        } catch (e) {\n            return {\n                code: -1,\n                msg: `Request failed: ${mapError(e)}`,\n            };\n        }\n    },\n};\n"
  },
  {
    "path": "src/module/Model/providers.ts",
    "content": "import { t } from \"../../lang\";\nimport BuildInProviderLogo from \"./../../assets/image/logo.svg\";\nimport ZhinaoProviderLogo from \"./assets/image/models/360.png\";\nimport HunyuanProviderLogo from \"./assets/image/models/hunyuan.png\";\nimport AzureProviderLogo from \"./assets/image/models/microsoft.png\";\nimport AiHubMixProviderLogo from \"./assets/image/providers/aihubmix.jpg\";\nimport AlayaNewProviderLogo from \"./assets/image/providers/alayanew.webp\";\nimport AnthropicProviderLogo from \"./assets/image/providers/anthropic.png\";\nimport BaichuanProviderLogo from \"./assets/image/providers/baichuan.png\";\nimport BaiduCloudProviderLogo from \"./assets/image/providers/baidu-cloud.svg\";\nimport BailianProviderLogo from \"./assets/image/providers/bailian.png\";\nimport DeepSeekProviderLogo from \"./assets/image/providers/deepseek.png\";\nimport DmxapiProviderLogo from \"./assets/image/providers/DMXAPI.png\";\nimport FireworksProviderLogo from \"./assets/image/providers/fireworks.png\";\nimport GiteeAIProviderLogo from \"./assets/image/providers/gitee-ai.png\";\nimport GithubProviderLogo from \"./assets/image/providers/github.png\";\nimport GoogleProviderLogo from \"./assets/image/providers/google.png\";\nimport GPUStackProviderLogo from \"./assets/image/providers/gpustack.svg\";\nimport GraphRagProviderLogo from \"./assets/image/providers/graph-rag.png\";\nimport GrokProviderLogo from \"./assets/image/providers/grok.png\";\nimport GroqProviderLogo from \"./assets/image/providers/groq.png\";\nimport HyperbolicProviderLogo from \"./assets/image/providers/hyperbolic.png\";\nimport InfiniProviderLogo from \"./assets/image/providers/infini.png\";\nimport JinaProviderLogo from \"./assets/image/providers/jina.png\";\nimport LMStudioProviderLogo from \"./assets/image/providers/lmstudio.png\";\nimport MinimaxProviderLogo from \"./assets/image/providers/minimax.png\";\nimport MistralProviderLogo from \"./assets/image/providers/mistral.png\";\nimport ModelScopeProviderLogo from \"./assets/image/providers/modelscope.png\";\nimport MoonshotProviderLogo from \"./assets/image/providers/moonshot.png\";\nimport NvidiaProviderLogo from \"./assets/image/providers/nvidia.png\";\nimport O3ProviderLogo from \"./assets/image/providers/o3.png\";\nimport OcoolAiProviderLogo from \"./assets/image/providers/ocoolai.png\";\nimport OllamaProviderLogo from \"./assets/image/providers/ollama.png\";\nimport OpenAiProviderLogo from \"./assets/image/providers/openai.png\";\nimport OpenRouterProviderLogo from \"./assets/image/providers/openrouter.png\";\nimport PerplexityProviderLogo from \"./assets/image/providers/perplexity.png\";\nimport PPIOProviderLogo from \"./assets/image/providers/ppio.png\";\nimport SiliconFlowProviderLogo from \"./assets/image/providers/silicon.png\";\nimport StepProviderLogo from \"./assets/image/providers/step.png\";\nimport TencentCloudProviderLogo from \"./assets/image/providers/tencent-cloud-ti.png\";\nimport TogetherProviderLogo from \"./assets/image/providers/together.png\";\nimport BytedanceProviderLogo from \"./assets/image/providers/volcengine.png\";\nimport VoyageAIProviderLogo from \"./assets/image/providers/voyageai.png\";\nimport XirangProviderLogo from \"./assets/image/providers/xirang.png\";\nimport ZeroOneProviderLogo from \"./assets/image/providers/zero-one.png\";\nimport ZhipuProviderLogo from \"./assets/image/providers/zhipu.png\";\nimport { ModelProvider } from \"./provider/provider\";\nimport { Provider } from \"./types\";\n\nconst ProviderLogoMap = {\n    buildIn: BuildInProviderLogo,\n    openai: OpenAiProviderLogo,\n    silicon: SiliconFlowProviderLogo,\n    deepseek: DeepSeekProviderLogo,\n    \"gitee-ai\": GiteeAIProviderLogo,\n    yi: ZeroOneProviderLogo,\n    groq: GroqProviderLogo,\n    zhipu: ZhipuProviderLogo,\n    ollama: OllamaProviderLogo,\n    lmstudio: LMStudioProviderLogo,\n    moonshot: MoonshotProviderLogo,\n    openrouter: OpenRouterProviderLogo,\n    baichuan: BaichuanProviderLogo,\n    dashscope: BailianProviderLogo,\n    modelscope: ModelScopeProviderLogo,\n    xirang: XirangProviderLogo,\n    anthropic: AnthropicProviderLogo,\n    aihubmix: AiHubMixProviderLogo,\n    gemini: GoogleProviderLogo,\n    stepfun: StepProviderLogo,\n    doubao: BytedanceProviderLogo,\n    \"graphrag-kylin-mountain\": GraphRagProviderLogo,\n    minimax: MinimaxProviderLogo,\n    github: GithubProviderLogo,\n    copilot: GithubProviderLogo,\n    ocoolai: OcoolAiProviderLogo,\n    together: TogetherProviderLogo,\n    fireworks: FireworksProviderLogo,\n    zhinao: ZhinaoProviderLogo,\n    nvidia: NvidiaProviderLogo,\n    \"azure-openai\": AzureProviderLogo,\n    hunyuan: HunyuanProviderLogo,\n    grok: GrokProviderLogo,\n    hyperbolic: HyperbolicProviderLogo,\n    mistral: MistralProviderLogo,\n    jina: JinaProviderLogo,\n    ppio: PPIOProviderLogo,\n    \"baidu-cloud\": BaiduCloudProviderLogo,\n    dmxapi: DmxapiProviderLogo,\n    perplexity: PerplexityProviderLogo,\n    infini: InfiniProviderLogo,\n    o3: O3ProviderLogo,\n    \"tencent-cloud-ti\": TencentCloudProviderLogo,\n    gpustack: GPUStackProviderLogo,\n    alayanew: AlayaNewProviderLogo,\n    voyageai: VoyageAIProviderLogo,\n} as const;\n\nexport function getProviderLogo(providerId: string) {\n    return ProviderLogoMap[providerId as keyof typeof ProviderLogoMap];\n}\n\nexport function getProviderUrl(provider: Provider) {\n    return ModelProvider.apiUrl(\n        provider.type,\n        provider.apiUrl,\n        provider.data.apiHost,\n    );\n}\n\nexport const getProviderTitle = (providerId: string) => {\n    const map = {\n        buildIn: \"provider.buildIn\",\n        aihubmix: \"AiHubMix\",\n        alayanew: \"Alaya NeW\",\n        anthropic: \"Anthropic\",\n        \"azure-openai\": \"Azure OpenAI\",\n        baichuan: \"provider.baichuan\",\n        \"baidu-cloud\": \"provider.baiduCloud\",\n        copilot: \"GitHub Copilot\",\n        dashscope: \"provider.dashscope\",\n        deepseek: \"provider.deepseek\",\n        dmxapi: \"DMXAPI\",\n        doubao: \"provider.doubao\",\n        fireworks: \"Fireworks\",\n        gemini: \"Gemini\",\n        \"gitee-ai\": \"Gitee AI\",\n        github: \"GitHub Models\",\n        gpustack: \"GPUStack\",\n        \"graphrag-kylin-mountain\": \"GraphRAG\",\n        grok: \"Grok\",\n        groq: \"Groq\",\n        hunyuan: \"provider.hunyuan\",\n        hyperbolic: \"Hyperbolic\",\n        infini: \"provider.infini\",\n        jina: \"Jina\",\n        lmstudio: \"LM Studio\",\n        minimax: \"MiniMax\",\n        mistral: \"Mistral\",\n        modelscope: \"provider.modelscope\",\n        moonshot: \"provider.moonshot\",\n        nvidia: \"provider.nvidia\",\n        o3: \"O3\",\n        ocoolai: \"ocoolAI\",\n        ollama: \"Ollama\",\n        openai: \"OpenAI\",\n        openrouter: \"OpenRouter\",\n        perplexity: \"Perplexity\",\n        ppio: \"provider.ppio\",\n        qwenlm: \"QwenLM\",\n        silicon: \"provider.silicon\",\n        stepfun: \"provider.stepfun\",\n        \"tencent-cloud-ti\": \"provider.tencentCloudTi\",\n        together: \"Together\",\n        xirang: \"provider.xirang\",\n        yi: \"provider.yi\",\n        zhinao: \"provider.zhinao\",\n        zhipu: \"provider.zhipu\",\n        voyageai: \"Voyage AI\",\n    };\n    const key = map[providerId as keyof typeof map];\n    if (!key) return providerId;\n    // Non-i18n entries (plain strings, not keys) start with capital/non-dot pattern\n    if (!key.includes(\".\")) return key;\n    return t(key) || key;\n};\n\nexport const SystemProviders = {\n    openai: {\n        api: {\n            url: \"https://api.openai.com\",\n        },\n        websites: {\n            official: \"https://openai.com/\",\n            apiKey: \"https://platform.openai.com/api-keys\",\n            docs: \"https://platform.openai.com/docs\",\n            models: \"https://platform.openai.com/docs/models\",\n        },\n    },\n    o3: {\n        api: {\n            url: \"https://api.o3.fan\",\n        },\n        websites: {\n            official: \"https://o3.fan\",\n            apiKey: \"https://o3.fan/token\",\n            docs: \"https://docs.o3.fan\",\n            models: \"https://docs.o3.fan/models\",\n        },\n    },\n    ppio: {\n        api: {\n            url: \"https://api.ppinfra.com/v3/openai\",\n        },\n        websites: {\n            official:\n                \"https://ppinfra.com/model-api/product/llm-api?utm_source=github_cherry-studio&utm_medium=github_readme&utm_campaign=link\",\n            apiKey: \"https://ppinfra.com/settings/key-management\",\n            docs: \"https://ppinfra.com/docs/model-api/reference/llm/llm.html\",\n            models: \"https://ppinfra.com/model-api/product/llm-api?utm_source=github_cherry-studio&utm_medium=github_readme&utm_campaign=link\",\n        },\n    },\n    gemini: {\n        api: {\n            url: \"https://generativelanguage.googleapis.com\",\n        },\n        websites: {\n            official: \"https://gemini.google.com/\",\n            apiKey: \"https://aistudio.google.com/app/apikey\",\n            docs: \"https://ai.google.dev/gemini-api/docs\",\n            models: \"https://ai.google.dev/gemini-api/docs/models/gemini\",\n        },\n    },\n    silicon: {\n        api: {\n            url: \"https://api.siliconflow.cn\",\n        },\n        websites: {\n            official: \"https://www.siliconflow.cn/\",\n            apiKey: \"https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88\",\n            docs: \"https://docs.siliconflow.cn/\",\n            models: \"https://docs.siliconflow.cn/docs/model-names\",\n        },\n    },\n    \"gitee-ai\": {\n        api: {\n            url: \"https://ai.gitee.com\",\n        },\n        websites: {\n            official: \"https://ai.gitee.com/\",\n            apiKey: \"https://ai.gitee.com/dashboard/settings/tokens\",\n            docs: \"https://ai.gitee.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90/POST/chat/completions\",\n            models: \"https://ai.gitee.com/serverless-api\",\n        },\n    },\n    deepseek: {\n        api: {\n            url: \"https://api.deepseek.com\",\n        },\n        websites: {\n            official: \"https://deepseek.com/\",\n            apiKey: \"https://platform.deepseek.com/api_keys\",\n            docs: \"https://platform.deepseek.com/api-docs/\",\n            models: \"https://platform.deepseek.com/api-docs/\",\n        },\n    },\n    ocoolai: {\n        api: {\n            url: \"https://api.ocoolai.com\",\n        },\n        websites: {\n            official: \"https://one.ocoolai.com/\",\n            apiKey: \"https://one.ocoolai.com/token\",\n            docs: \"https://docs.ocoolai.com/\",\n            models: \"https://api.ocoolai.com/info/models/\",\n        },\n    },\n    together: {\n        api: {\n            url: \"https://api.together.xyz\",\n        },\n        websites: {\n            official: \"https://www.together.ai/\",\n            apiKey: \"https://api.together.ai/settings/api-keys\",\n            docs: \"https://docs.together.ai/docs/introduction\",\n            models: \"https://docs.together.ai/docs/chat-models\",\n        },\n    },\n    dmxapi: {\n        api: {\n            url: \"https://www.dmxapi.cn\",\n        },\n        websites: {\n            official: \"https://www.dmxapi.cn/register?aff=bwwY\",\n            apiKey: \"https://www.dmxapi.cn/register?aff=bwwY\",\n            docs: \"https://dmxapi.cn/models.html#code-block\",\n            models: \"https://www.dmxapi.cn/pricing\",\n        },\n    },\n    perplexity: {\n        api: {\n            url: \"https://api.perplexity.ai/\",\n        },\n        websites: {\n            official: \"https://perplexity.ai/\",\n            apiKey: \"https://www.perplexity.ai/settings/api\",\n            docs: \"https://docs.perplexity.ai/home\",\n            models: \"https://docs.perplexity.ai/guides/model-cards\",\n        },\n    },\n    infini: {\n        api: {\n            url: \"https://cloud.infini-ai.com/maas\",\n        },\n        websites: {\n            official: \"https://cloud.infini-ai.com/\",\n            apiKey: \"https://cloud.infini-ai.com/iam/secret/key\",\n            docs: \"https://docs.infini-ai.com/gen-studio/api/maas.html#/operations/chatCompletions\",\n            models: \"https://cloud.infini-ai.com/genstudio/model\",\n        },\n    },\n    github: {\n        api: {\n            url: \"https://models.inference.ai.azure.com/\",\n        },\n        websites: {\n            official: \"https://github.com/marketplace/models\",\n            apiKey: \"https://github.com/settings/tokens\",\n            docs: \"https://docs.github.com/en/github-models\",\n            models: \"https://github.com/marketplace/models\",\n        },\n    },\n    copilot: {\n        api: {\n            url: \"https://api.githubcopilot.com/\",\n        },\n    },\n    yi: {\n        api: {\n            url: \"https://api.lingyiwanwu.com\",\n        },\n        websites: {\n            official: \"https://platform.lingyiwanwu.com/\",\n            apiKey: \"https://platform.lingyiwanwu.com/apikeys\",\n            docs: \"https://platform.lingyiwanwu.com/docs\",\n            models: \"https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B\",\n        },\n    },\n    zhipu: {\n        api: {\n            url: \"https://open.bigmodel.cn/api/paas/v4/\",\n        },\n        websites: {\n            official: \"https://open.bigmodel.cn/\",\n            apiKey: \"https://open.bigmodel.cn/usercenter/apikeys\",\n            docs: \"https://open.bigmodel.cn/dev/howuse/introduction\",\n            models: \"https://open.bigmodel.cn/modelcenter/square\",\n        },\n    },\n    moonshot: {\n        api: {\n            url: \"https://api.moonshot.cn\",\n        },\n        websites: {\n            official: \"https://moonshot.ai/\",\n            apiKey: \"https://platform.moonshot.cn/console/api-keys\",\n            docs: \"https://platform.moonshot.cn/docs/\",\n            models: \"https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8\",\n        },\n    },\n    baichuan: {\n        api: {\n            url: \"https://api.baichuan-ai.com\",\n        },\n        websites: {\n            official: \"https://www.baichuan-ai.com/\",\n            apiKey: \"https://platform.baichuan-ai.com/console/apikey\",\n            docs: \"https://platform.baichuan-ai.com/docs\",\n            models: \"https://platform.baichuan-ai.com/price\",\n        },\n    },\n    modelscope: {\n        api: {\n            url: \"https://api-inference.modelscope.cn/v1/\",\n        },\n        websites: {\n            official: \"https://modelscope.cn\",\n            apiKey: \"https://modelscope.cn/my/myaccesstoken\",\n            docs: \"https://modelscope.cn/docs/model-service/API-Inference/intro\",\n            models: \"https://modelscope.cn/models\",\n        },\n    },\n    xirang: {\n        api: {\n            url: \"https://wishub-x1.ctyun.cn\",\n        },\n        websites: {\n            official: \"https://www.ctyun.cn\",\n            apiKey: \"https://huiju.ctyun.cn/service/serviceGroup\",\n            docs: \"https://www.ctyun.cn/products/ctxirang\",\n            models: \"https://huiju.ctyun.cn/modelSquare/\",\n        },\n    },\n    dashscope: {\n        api: {\n            url: \"https://dashscope.aliyuncs.com/compatible-mode/v1/\",\n        },\n        websites: {\n            official: \"https://www.aliyun.com/product/bailian\",\n            apiKey: \"https://bailian.console.aliyun.com/?apiKey=1#/api-key\",\n            docs: \"https://help.aliyun.com/zh/model-studio/getting-started/\",\n            models: \"https://bailian.console.aliyun.com/model-market#/model-market\",\n        },\n    },\n    stepfun: {\n        api: {\n            url: \"https://api.stepfun.com\",\n        },\n        websites: {\n            official: \"https://platform.stepfun.com/\",\n            apiKey: \"https://platform.stepfun.com/interface-key\",\n            docs: \"https://platform.stepfun.com/docs/overview/concept\",\n            models: \"https://platform.stepfun.com/docs/llm/text\",\n        },\n    },\n    doubao: {\n        api: {\n            url: \"https://ark.cn-beijing.volces.com/api/v3/\",\n        },\n        websites: {\n            official: \"https://console.volcengine.com/ark/\",\n            apiKey: \"https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=DB4II4FC\",\n            docs: \"https://www.volcengine.com/docs/82379/1182403\",\n            models: \"https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint\",\n        },\n    },\n    minimax: {\n        api: {\n            url: \"https://api.minimax.chat/v1/\",\n        },\n        websites: {\n            official: \"https://platform.minimaxi.com/\",\n            apiKey: \"https://platform.minimaxi.com/user-center/basic-information/interface-key\",\n            docs: \"https://platform.minimaxi.com/document/Announcement\",\n            models: \"https://platform.minimaxi.com/document/Models\",\n        },\n    },\n    alayanew: {\n        api: {\n            url: \"https://deepseek.alayanew.com\",\n        },\n        websites: {\n            official:\n                \"https://www.alayanew.com/backend/register?id=cherrystudio\",\n            apiKey: \" https://www.alayanew.com/backend/register?id=cherrystudio\",\n            docs: \"https://docs.alayanew.com/docs/modelService/interview?utm_source=cherrystudio\",\n            models: \"https://www.alayanew.com/product/deepseek?id=cherrystudio\",\n        },\n    },\n    openrouter: {\n        api: {\n            url: \"https://openrouter.ai/api/v1/\",\n        },\n        websites: {\n            official: \"https://openrouter.ai/\",\n            apiKey: \"https://openrouter.ai/settings/keys\",\n            docs: \"https://openrouter.ai/docs/quick-start\",\n            models: \"https://openrouter.ai/docs/models\",\n        },\n    },\n    groq: {\n        api: {\n            url: \"https://api.groq.com/openai\",\n        },\n        websites: {\n            official: \"https://groq.com/\",\n            apiKey: \"https://console.groq.com/keys\",\n            docs: \"https://console.groq.com/docs/quickstart\",\n            models: \"https://console.groq.com/docs/models\",\n        },\n    },\n    ollama: {\n        api: {\n            url: \"http://localhost:11434\",\n        },\n        websites: {\n            official: \"https://ollama.com/\",\n            docs: \"https://github.com/ollama/ollama/tree/main/docs\",\n            models: \"https://ollama.com/library\",\n        },\n    },\n    lmstudio: {\n        api: {\n            url: \"http://localhost:1234\",\n        },\n        websites: {\n            official: \"https://lmstudio.ai/\",\n            docs: \"https://lmstudio.ai/docs\",\n            models: \"https://lmstudio.ai/models\",\n        },\n    },\n    anthropic: {\n        api: {\n            url: \"https://api.anthropic.com/\",\n        },\n        websites: {\n            official: \"https://anthropic.com/\",\n            apiKey: \"https://console.anthropic.com/settings/keys\",\n            docs: \"https://docs.anthropic.com/en/docs\",\n            models: \"https://docs.anthropic.com/en/docs/about-claude/models\",\n        },\n    },\n    grok: {\n        api: {\n            url: \"https://api.x.ai\",\n        },\n        websites: {\n            official: \"https://x.ai/\",\n            docs: \"https://docs.x.ai/\",\n            models: \"https://docs.x.ai/docs#getting-started\",\n        },\n    },\n    hyperbolic: {\n        api: {\n            url: \"https://api.hyperbolic.xyz\",\n        },\n        websites: {\n            official: \"https://app.hyperbolic.xyz\",\n            apiKey: \"https://app.hyperbolic.xyz/settings\",\n            docs: \"https://docs.hyperbolic.xyz\",\n            models: \"https://app.hyperbolic.xyz/models\",\n        },\n    },\n    mistral: {\n        api: {\n            url: \"https://api.mistral.ai\",\n        },\n        websites: {\n            official: \"https://mistral.ai\",\n            apiKey: \"https://console.mistral.ai/api-keys/\",\n            docs: \"https://docs.mistral.ai\",\n            models: \"https://docs.mistral.ai/getting-started/models/models_overview\",\n        },\n    },\n    jina: {\n        api: {\n            url: \"https://api.jina.ai\",\n        },\n        websites: {\n            official: \"https://jina.ai\",\n            apiKey: \"https://jina.ai/\",\n            docs: \"https://jina.ai\",\n            models: \"https://jina.ai\",\n        },\n    },\n    aihubmix: {\n        api: {\n            url: \"https://aihubmix.com\",\n        },\n        websites: {\n            official: \"https://aihubmix.com?aff=SJyh\",\n            apiKey: \"https://aihubmix.com?aff=SJyh\",\n            docs: \"https://doc.aihubmix.com/\",\n            models: \"https://aihubmix.com/models\",\n        },\n    },\n    fireworks: {\n        api: {\n            url: \"https://api.fireworks.ai/inference\",\n        },\n        websites: {\n            official: \"https://fireworks.ai/\",\n            apiKey: \"https://fireworks.ai/account/api-keys\",\n            docs: \"https://docs.fireworks.ai/getting-started/introduction\",\n            models: \"https://fireworks.ai/dashboard/models\",\n        },\n    },\n    zhinao: {\n        api: {\n            url: \"https://api.360.cn\",\n        },\n        websites: {\n            official: \"https://ai.360.com/\",\n            apiKey: \"https://ai.360.com/platform/keys\",\n            docs: \"https://ai.360.com/platform/docs/overview\",\n            models: \"https://ai.360.com/platform/limit\",\n        },\n    },\n    hunyuan: {\n        api: {\n            url: \"https://api.hunyuan.cloud.tencent.com\",\n        },\n        websites: {\n            official: \"https://cloud.tencent.com/product/hunyuan\",\n            apiKey: \"https://console.cloud.tencent.com/hunyuan/api-key\",\n            docs: \"https://cloud.tencent.com/document/product/1729/111007\",\n            models: \"https://cloud.tencent.com/document/product/1729/104753\",\n        },\n    },\n    nvidia: {\n        api: {\n            url: \"https://integrate.api.nvidia.com\",\n        },\n        websites: {\n            official: \"https://build.nvidia.com/explore/discover\",\n            apiKey: \"https://build.nvidia.com/meta/llama-3_1-405b-instruct\",\n            docs: \"https://docs.api.nvidia.com/nim/reference/llm-apis\",\n            models: \"https://build.nvidia.com/nim\",\n        },\n    },\n    \"azure-openai\": {\n        api: {\n            url: \"\",\n        },\n        websites: {\n            official:\n                \"https://azure.microsoft.com/en-us/products/ai-services/openai-service\",\n            apiKey: \"https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/OpenAI\",\n            docs: \"https://learn.microsoft.com/en-us/azure/ai-services/openai/\",\n            models: \"https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models\",\n        },\n    },\n    \"baidu-cloud\": {\n        api: {\n            url: \"https://qianfan.baidubce.com/v2/\",\n        },\n        websites: {\n            official: \"https://cloud.baidu.com/\",\n            apiKey: \"https://console.bce.baidu.com/iam/#/iam/apikey/list\",\n            docs: \"https://cloud.baidu.com/doc/index.html\",\n            models: \"https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu\",\n        },\n    },\n    \"tencent-cloud-ti\": {\n        api: {\n            url: \"https://api.lkeap.cloud.tencent.com\",\n        },\n        websites: {\n            official: \"https://cloud.tencent.com/product/ti\",\n            apiKey: \"https://console.cloud.tencent.com/lkeap/api\",\n            docs: \"https://cloud.tencent.com/document/product/1772\",\n            models: \"https://console.cloud.tencent.com/tione/v2/aimarket\",\n        },\n    },\n    gpustack: {\n        api: {\n            url: \"\",\n        },\n        websites: {\n            official: \"https://gpustack.ai/\",\n            docs: \"https://docs.gpustack.ai/latest/\",\n            models: \"https://docs.gpustack.ai/latest/overview/#supported-models\",\n        },\n    },\n    voyageai: {\n        api: {\n            url: \"https://api.voyageai.com\",\n        },\n        websites: {\n            official: \"https://www.voyageai.com/\",\n            apiKey: \"https://dashboard.voyageai.com/organization/api-keys\",\n            docs: \"https://docs.voyageai.com/docs\",\n            models: \"https://docs.voyageai.com/docs\",\n        },\n    },\n};\n"
  },
  {
    "path": "src/module/Model/store/model.ts",
    "content": "import { defineStore } from \"pinia\";\nimport store from \"../../../store/index\";\n\nimport { debounce } from \"lodash-es\";\nimport { watch } from \"vue\";\nimport { AppConfig } from \"../../../config\";\nimport { t } from \"../../../lang\";\nimport { Dialog } from \"../../../lib/dialog\";\nimport { mapError } from \"../../../lib/error\";\nimport { ObjectUtil, StringUtil } from \"../../../lib/util\";\nimport { useUserStore } from \"../../../store/modules/user\";\nimport { SystemModels } from \"../models\";\nimport { ModelChatResult, ModelProvider } from \"../provider/provider\";\nimport {\n    getProviderLogo,\n    getProviderTitle,\n    SystemProviders,\n} from \"../providers\";\nimport { ChatParam, Model, Provider } from \"../types\";\n\nconst userStore = useUserStore();\n\nexport type ModelItem = {\n    id: string;\n    providerId: string;\n    providerLogo: string;\n    providerTitle: string;\n    modelId: string;\n    modelName: string;\n};\n\nwatch(\n    () => userStore.data,\n    (newValue) => {\n        model.init().then();\n    },\n    {\n        deep: true,\n    },\n);\n\nconst mapModelError = (e: unknown, provider: Provider) => {\n    if (provider.id === \"buildIn\") {\n        const msg = String(e);\n        const showCharge = () => {\n            $mapi.user\n                .open({\n                    readyParam: {\n                        page: \"ChargeLmApi\",\n                    },\n                })\n                .then();\n        };\n        const map = {\n            insufficient_user_quota: {\n                msg: t(\"error.energyInsufficient\"),\n                callback: showCharge,\n            },\n        };\n        for (const key in map) {\n            if (msg.includes(key)) {\n                const error = map[key];\n                if (error.callback) {\n                    setTimeout(() => {\n                        error.callback();\n                    }, 3000);\n                }\n                return error.msg;\n            }\n        }\n    }\n    return mapError(e);\n};\n\nexport const modelStore = defineStore(\"model\", {\n    state() {\n        return {\n            providers: [] as Provider[],\n        };\n    },\n    actions: {\n        async init() {\n            const results: Provider[] = [];\n            for (const providerId in SystemProviders) {\n                const provider = SystemProviders[providerId];\n                results.push({\n                    id: providerId,\n                    type: \"openai\",\n                    title: getProviderTitle(providerId),\n                    logo: getProviderLogo(providerId),\n                    isSystem: true,\n                    apiUrl: provider.api.url,\n                    websites: {\n                        official: provider.websites?.official,\n                        docs: provider.websites?.docs,\n                        models: provider.websites?.models,\n                    },\n                    data: {\n                        apiKey: \"\",\n                        apiHost: \"\",\n                        models: (SystemModels[providerId] || []).map((m) => {\n                            return {\n                                id: m.id,\n                                provider: providerId,\n                                name: m.name,\n                                group: m.group,\n                                types: [\"text\" as Model[\"types\"][number]],\n                                enabled: false,\n                                editable: false,\n                            } satisfies Model;\n                        }),\n                        enabled: false,\n                    },\n                });\n            }\n            let buildInProviderData: Partial<Provider[\"data\"]> | null = null;\n            const storageData = await $mapi.storage.read(\"models\");\n            if (storageData) {\n                if (storageData.userProviders) {\n                    storageData.userProviders.forEach((provider) => {\n                        results.unshift({\n                            id: provider.id,\n                            type: provider.type,\n                            title: provider.title,\n                            logo: null,\n                            isSystem: false,\n                            apiUrl: \"\",\n                            websites: {\n                                official: \"\",\n                                docs: \"\",\n                                models: \"\",\n                            },\n                            data: {\n                                apiKey: \"\",\n                                apiHost: \"\",\n                                models: [],\n                                enabled: false,\n                            },\n                        });\n                    });\n                }\n                if (storageData.providerData) {\n                    buildInProviderData =\n                        storageData.providerData[\"buildIn\"] || null;\n                    for (const providerId in storageData.providerData) {\n                        const provider = results.find(\n                            (p) => p.id === providerId,\n                        );\n                        if (provider) {\n                            provider.data.apiKey =\n                                storageData.providerData[providerId].apiKey ||\n                                \"\";\n                            provider.data.apiHost =\n                                storageData.providerData[providerId].apiHost;\n                            (\n                                storageData.providerData[providerId].models ||\n                                []\n                            ).forEach((model) => {\n                                const existingModel = provider.data.models.find(\n                                    (m) => m.id === model.id,\n                                );\n                                if (existingModel) {\n                                    existingModel.name = model.name;\n                                    existingModel.group = model.group;\n                                    existingModel.types = model.types;\n                                    existingModel.enabled =\n                                        model.enabled || false;\n                                } else {\n                                    provider.data.models.push({\n                                        id: model.id,\n                                        provider: providerId,\n                                        name: model.name,\n                                        group: model.group,\n                                        types: [\"text\"],\n                                        enabled: model.enabled || false,\n                                        editable: true,\n                                    });\n                                }\n                            });\n                            provider.data.enabled =\n                                storageData.providerData[providerId].enabled ||\n                                false;\n                        }\n                    }\n                }\n            }\n            this.providers = results;\n            await this.refreshBuildIn(buildInProviderData);\n        },\n        async enabledModels(): Promise<ModelItem[]> {\n            const results: ModelItem[] = [];\n            this.providers.forEach((provider) => {\n                if (provider.data.enabled) {\n                    provider.data.models.forEach((model) => {\n                        if (model.enabled) {\n                            results.push({\n                                id: provider.id + \"|\" + model.id,\n                                providerId: provider.id,\n                                providerLogo: provider.logo || \"\",\n                                providerTitle: provider.title || \"\",\n                                modelId: model.id,\n                                modelName: model.name,\n                            });\n                        }\n                    });\n                }\n            });\n            return results;\n        },\n        async refreshBuildIn(\n            buildInProviderData?: Partial<Provider[\"data\"]> | null,\n        ) {\n            if (\n                userStore.data &&\n                userStore.data.lmApi &&\n                userStore.data.lmApi.models\n            ) {\n                const lmApi = userStore.data.lmApi;\n                const buildInProvider = this.providers.find(\n                    (p) => p.id === \"buildIn\",\n                );\n                if (!buildInProvider) {\n                    const models: Model[] = [];\n                    for (const m of lmApi.models) {\n                        models.push({\n                            id: m,\n                            provider: \"buildIn\",\n                            name: m,\n                            group: \"Default\",\n                            types: [\"text\"],\n                            enabled: true,\n                            editable: false,\n                        });\n                    }\n                    // console.log(\"model.init.buildIn\", JSON.stringify({lmApi}, null, 2));\n                    let enabled = true;\n                    if (\n                        buildInProviderData &&\n                        \"enabled\" in buildInProviderData\n                    ) {\n                        enabled = buildInProviderData.enabled ?? true;\n                    }\n                    console.log(\"model.init.buildIn\", {\n                        enabled,\n                        buildInProviderData,\n                    });\n                    this.providers.unshift({\n                        id: \"buildIn\",\n                        type: \"openai\",\n                        title: getProviderTitle(\"buildIn\"),\n                        logo: getProviderLogo(\"buildIn\"),\n                        isSystem: true,\n                        apiUrl: lmApi.apiUrl,\n                        websites: {\n                            official: AppConfig.website,\n                            docs: AppConfig.website,\n                            models: AppConfig.website,\n                        },\n                        data: {\n                            apiKey: lmApi.apiKey,\n                            apiHost: \"\",\n                            models: models,\n                            enabled: enabled,\n                        },\n                    });\n                } else {\n                    buildInProvider.data.apiKey = lmApi.apiKey;\n                }\n            }\n        },\n        async add(provider: Partial<Provider>) {\n            const p: Provider = {\n                id: provider.id || StringUtil.random(8),\n                type: provider.type || \"openai\",\n                title: provider.title || \"\",\n                logo: null,\n                isSystem: false,\n                apiUrl: \"\",\n                websites: {\n                    official: \"\",\n                    docs: \"\",\n                    models: \"\",\n                },\n                data: {\n                    apiKey: \"\",\n                    apiHost: \"\",\n                    models: [],\n                    enabled: false,\n                },\n            };\n            this.providers.unshift(p);\n            await this.sync();\n        },\n        async edit(provider: Partial<Provider>) {\n            const p = this.providers.find((p) => p.id === provider.id);\n            if (p) {\n                if (\"title\" in provider) {\n                    p.title = provider.title || \"\";\n                }\n                if (\"type\" in provider) {\n                    p.type = provider.type || \"openai\";\n                }\n                if (provider.data) {\n                    if (\"apiKey\" in provider.data) {\n                        p.data.apiKey = provider.data.apiKey;\n                    }\n                    if (\"apiHost\" in provider.data) {\n                        p.data.apiHost = provider.data.apiHost;\n                    }\n                    if (\"enabled\" in provider.data) {\n                        p.data.enabled = provider.data.enabled;\n                    }\n                }\n                await this.sync();\n            }\n        },\n        async test(providerId: string, modelId: string) {\n            await this.refreshBuildIn();\n            const provider = this.providers.find((p) => p.id === providerId);\n            if (!provider) {\n                return;\n            }\n            const m = provider.data.models.find((m) => m.id === modelId);\n            if (!m) {\n                return;\n            }\n            Dialog.loadingOn(t(\"common.testing\"));\n            try {\n                const ret = await ModelProvider.chat(\n                    t(\"model.testPrompt\"),\n                    {\n                        systemPrompt: null,\n                    },\n                    {\n                        type: provider.type,\n                        modelId: m.id,\n                        apiUrl: provider.apiUrl,\n                        apiHost: provider.data.apiHost,\n                        apiKey: provider.data.apiKey,\n                    },\n                );\n                if (ret.code) {\n                    throw ret.msg;\n                }\n                Dialog.tipSuccess(t(\"common.testSuccess\"));\n            } catch (e) {\n                Dialog.tipError(\n                    t(\"common.testFailed\") + \" \" + mapModelError(e, provider),\n                );\n            } finally {\n                Dialog.loadingOff();\n            }\n        },\n        async chat(\n            providerId: string,\n            modelId: string,\n            prompt: string,\n            chatParam: ChatParam,\n            option?: {\n                loading: boolean;\n            },\n        ): Promise<ModelChatResult> {\n            await this.refreshBuildIn();\n            if (!providerId || !modelId) {\n                Dialog.tipError(t(\"hint.selectModel\"));\n                return { code: -1, msg: t(\"hint.selectModel\") };\n            }\n            option = Object.assign(\n                {\n                    loading: false,\n                },\n                option,\n            );\n            const provider = this.providers.find((p) => p.id === providerId);\n            // console.log(\"provider.chat\", JSON.stringify({provider}, null, 2));\n            if (!provider) {\n                return { code: -1, msg: \"provider not found\" };\n            }\n            const m = provider.data.models.find((m) => m.id === modelId);\n            if (!m) {\n                return { code: -1, msg: \"model not found\" };\n            }\n            if (option.loading) {\n                Dialog.loadingOn();\n            }\n            try {\n                return await ModelProvider.chat(prompt, chatParam, {\n                    type: provider.type,\n                    modelId: m.id,\n                    apiUrl: provider.apiUrl,\n                    apiHost: provider.data.apiHost,\n                    apiKey: provider.data.apiKey,\n                });\n            } catch (e) {\n                return { code: -1, msg: mapModelError(e, provider) };\n            } finally {\n                if (option.loading) {\n                    Dialog.loadingOff();\n                }\n            }\n        },\n        async change(\n            providerId: string,\n            key: \"\" | \"data.apiKey\" | \"data.apiHost\" | \"data.enabled\",\n            value: string | boolean,\n        ) {\n            const provider = model.providers.find((p) => p.id === providerId);\n            if (!provider) {\n                return;\n            }\n            const keys = key.split(\".\");\n            let obj = provider;\n            for (let i = 0; i < keys.length - 1; i++) {\n                obj = obj[keys[i]];\n            }\n            const lastKey = keys[keys.length - 1];\n            if (obj && lastKey in obj) {\n                obj[lastKey] = value;\n            }\n            await this.sync();\n        },\n        async modelAdd(providerId: string, model: Partial<Model>) {\n            const provider = this.providers.find((p) => p.id === providerId);\n            if (!provider) {\n                return;\n            }\n            const m: Model = {\n                id: model.id || StringUtil.random(8),\n                provider: providerId,\n                name: model.name || \"\",\n                group: model.group || \"\",\n                types: model.types || [\"text\"],\n                enabled: true,\n                editable: model.editable ?? true,\n            };\n            provider.data.models.unshift(m);\n            await this.sync();\n        },\n        async modelDelete(providerId: string, modelId: string) {\n            const provider = this.providers.find((p) => p.id === providerId);\n            if (!provider) {\n                return;\n            }\n            const m = provider.data.models.find((m) => m.id === modelId);\n            if (m) {\n                provider.data.models.splice(provider.data.models.indexOf(m), 1);\n            }\n            await this.sync();\n        },\n        async modelEdit(providerId: string, model: Partial<Model>) {\n            const provider = this.providers.find((p) => p.id === providerId);\n            if (!provider) {\n                return;\n            }\n            const m = provider.data.models.find((m) => m.id === model.id);\n            if (m) {\n                if (\"name\" in model) {\n                    m.name = model.name || \"\";\n                }\n                if (\"group\" in model) {\n                    m.group = model.group || \"\";\n                }\n                if (\"types\" in model) {\n                    m.types = model.types || [\"text\"];\n                }\n                if (\"enabled\" in model) {\n                    m.enabled = model.enabled as boolean;\n                }\n            }\n            await this.sync();\n        },\n        async changeModel(\n            providerId: string,\n            modelId: string,\n            key: \"enabled\",\n            value: boolean,\n        ) {\n            const provider = this.providers.find((p) => p.id === providerId);\n            if (!provider) {\n                return;\n            }\n            const m = provider.data.models.find((m) => m.id === modelId);\n            if (m) {\n                m[key] = value;\n            }\n            await this.sync();\n        },\n        sync: debounce(async () => {\n            const providerData = {};\n            model.providers.forEach((provider) => {\n                providerData[provider.id] = ObjectUtil.clone(provider.data);\n                if (provider.id === \"buildIn\") {\n                    providerData[provider.id].apiKey = \"\";\n                }\n            });\n            const userProviders = model.providers.filter(\n                (provider) => !provider.isSystem,\n            );\n            await $mapi.storage.write(\n                \"models\",\n                ObjectUtil.clone({ providerData, userProviders }),\n            );\n        }, 200),\n    },\n});\n\nexport const model = modelStore(store);\nmodel.init().then();\n\nexport const useModelStore = () => {\n    return model;\n};\n"
  },
  {
    "path": "src/module/Model/types.ts",
    "content": "export type ProviderType = \"openai\"; // | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'\n\nexport type ModelType = \"text\"; // | 'vision' | 'embedding' | 'reasoning' | 'function_calling'\n\nexport type Model = {\n    id: string;\n    provider: string;\n    name: string;\n    group: string;\n    types: ModelType[];\n    enabled: boolean;\n    editable: boolean;\n};\n\nexport type Provider = {\n    id: string;\n    type: ProviderType;\n    logo: string | null;\n    title: string;\n    isSystem: boolean;\n    apiUrl: string;\n    websites: {\n        official: string;\n        docs: string;\n        models: string;\n    };\n    data: {\n        apiKey: string;\n        apiHost: string;\n        models: Model[];\n        enabled: boolean;\n    };\n    runtime?: {};\n};\n\nexport type ChatParam = {\n    systemPrompt: string | null;\n};\n"
  },
  {
    "path": "src/pages/DetachWindow/operate.ts",
    "content": "import { Menu } from \"@electron/remote\";\nimport { t } from \"../../lang\";\n\nexport const useDetachWindowOperate = ({ plugin }) => {\n    const doShowZoomMenu = () => {\n        const menuTemplate: any[] = [];\n        const zoomPercent = [\n            50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300,\n        ];\n        for (let z of zoomPercent) {\n            menuTemplate.push({\n                label: `${z}%`,\n                click: async () => {\n                    await window.$mapi.manager.setDetachPluginZoom(z);\n                    plugin.value.runtime.config.zoom = z;\n                },\n            });\n        }\n        Menu.buildFromTemplate(menuTemplate).popup();\n    };\n\n    const doShowMoreMenu = () => {\n        const autoDetach = !!plugin.value.runtime.config.autoDetach;\n        const menuTemplate: any[] = [];\n        menuTemplate.push({\n            label: t(\"plugin.debugWindow\"),\n            click: async () => {\n                await window.$mapi.manager.openDetachPluginDevTools();\n            },\n        });\n        menuTemplate.push({\n            label: t(\"plugin.backendLog\"),\n            click: async () => {\n                await window.$mapi.manager.openDetachPluginLog();\n            },\n        });\n        if (!(plugin.value.setting && plugin.value.setting.autoDetach)) {\n            menuTemplate.push({\n                label: t(\"plugin.autoDetachWindow\"),\n                type: \"checkbox\",\n                checked: autoDetach,\n                click: async () => {\n                    await window.$mapi.manager.setPluginAutoDetach(!autoDetach);\n                    plugin.value.runtime.config =\n                        await window.$mapi.manager.getPluginConfig(\n                            plugin.value.name,\n                        );\n                },\n            });\n        }\n        if (plugin.value.setting) {\n            if (\n                plugin.value.setting.moreMenu &&\n                plugin.value.setting.moreMenu.length > 0\n            ) {\n                for (const item of plugin.value.setting.moreMenu) {\n                    ((item) => {\n                        menuTemplate.push({\n                            label: item.title,\n                            click: async () => {\n                                await window.$mapi.manager.firePluginMoreMenuClick(\n                                    item.name,\n                                );\n                            },\n                        });\n                    })(item);\n                }\n            }\n        }\n        Menu.buildFromTemplate(menuTemplate).popup();\n    };\n\n    const doClose = async () => {\n        await window.$mapi.manager.closeDetachPlugin();\n    };\n\n    return {\n        doShowZoomMenu,\n        doShowMoreMenu,\n        doClose,\n    };\n};\n"
  },
  {
    "path": "src/pages/FastPanel/FastPanelResult.vue",
    "content": "<script setup lang=\"ts\">\nimport { useManagerStore } from \"../../store/modules/manager\";\nimport { PluginType } from \"../../types/Manager\";\nimport { useViewOperate } from \"../Main/Lib/viewOperate\";\nimport { useResultOperate } from \"./Lib/resultOperate\";\n\nconst manager = useManagerStore();\nconst { doOpenAction } = useResultOperate();\n\nconst { webUserAgent, viewActions } = useViewOperate(\"fastPanel\");\n</script>\n\n<template>\n    <div class=\"pb-fastpanel-result\">\n        <div style=\"height: 40px\"></div>\n        <div class=\"view\" v-if=\"viewActions.length\">\n            <div v-for=\"r in viewActions\" class=\"view-item\">\n                <div class=\"view-item-head\">\n                    <div class=\"icon\">\n                        <img\n                            :src=\"r.icon\"\n                            :class=\"\n                                r.pluginType === PluginType.SYSTEM\n                                    ? 'dark:invert'\n                                    : 'plugin-logo-filter'\n                            \"\n                        />\n                    </div>\n                    <div class=\"text\">\n                        {{ r.title }}\n                    </div>\n                    <div v-if=\"0\" class=\"action\">\n                        <a href=\"javascript:;\"> {{ $t(\"common.close\") }} </a>\n                        <a href=\"javascript:;\">\n                            <icon-more-vertical />\n                        </a>\n                    </div>\n                </div>\n                <div class=\"view-item-body\">\n                    <webview\n                        class=\"web\"\n                        :ref=\"(el) => (r['_web'] = el)\"\n                        :style=\"{ height: r['_height'] + 'px' }\"\n                        :id=\"r.fullName\"\n                        :preload=\"r.runtime?.view?.preloadBase\"\n                        :src=\"r.runtime?.view?.mainView\"\n                        :nodeintegration=\"r.runtime?.view?.nodeIntegration\"\n                        :useragent=\"`${webUserAgent} PluginAction/${r.fullName}`\"\n                        webpreferences=\"contextIsolation=false,sandbox=false\"\n                        disablewebsecurity\n                    ></webview>\n                    <div class=\"view-item-loading\" v-if=\"!r['_webReady']\">\n                        <icon-loading />\n                    </div>\n                </div>\n            </div>\n        </div>\n        <div class=\"action\">\n            <div v-for=\"a in manager.fastPanelMatchActions\" class=\"action-item\">\n                <div class=\"action-item-box\" @click=\"doOpenAction(a)\">\n                    <div class=\"icon\">\n                        <img\n                            :src=\"a.icon\"\n                            draggable=\"false\"\n                            :class=\"\n                                a.pluginType === PluginType.SYSTEM\n                                    ? 'dark:invert'\n                                    : 'plugin-logo-filter'\n                            \"\n                        />\n                    </div>\n                    <div class=\"text\">\n                        {{ a.title }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style lang=\"less\">\n[data-theme=\"dark\"] {\n    .pb-fastpanel-result {\n        .action {\n            .action-item-box {\n                &:hover {\n                    background: #2a2a2b !important;\n                }\n            }\n        }\n    }\n}\n\n.pb-fastpanel-result {\n    user-select: none;\n\n    .view-item-head {\n        display: flex;\n        align-items: center;\n        padding: 2px 5px;\n        font-size: 12px;\n        height: 26px;\n\n        &:hover {\n            .action {\n                display: block;\n            }\n        }\n\n        .icon {\n            width: 16px;\n            height: 16px;\n            margin-right: 5px;\n\n            img {\n                width: 16px;\n                height: 16px;\n                object-fit: contain;\n            }\n        }\n\n        .text {\n            flex: 1;\n            user-select: none;\n        }\n\n        .action {\n            a {\n                display: inline-block;\n                border-radius: 5px;\n                height: 20px;\n                min-width: 20px;\n                text-align: center;\n                font-size: 10px;\n                line-height: 20px;\n                padding: 0 5px;\n                margin-left: 2px;\n\n                &:hover {\n                    background: #f0f0f0;\n                }\n            }\n        }\n    }\n\n    .view-item-body {\n        position: relative;\n\n        .web {\n            transition: height 0.3s;\n        }\n\n        .view-item-loading {\n            position: absolute;\n            inset: 0;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            background: rgba(255, 255, 255, 0.8);\n            z-index: 1;\n        }\n    }\n\n    .action-item-box {\n        text-align: center;\n        padding: 10px 0 0 0;\n        border-radius: 10px;\n        cursor: pointer;\n\n        &:hover {\n            background-color: #f8f8f8;\n        }\n\n        &:active {\n            background-color: #e8e8e8;\n        }\n\n        .icon {\n            text-align: center;\n\n            img {\n                width: 30px;\n                height: 30px;\n                object-fit: contain;\n                margin: 0 auto;\n            }\n        }\n\n        .text {\n            margin-top: 5px;\n            height: 32px;\n            line-height: 16px;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            padding: 0 5px;\n            font-size: 13px;\n        }\n    }\n\n    .view {\n        .view-item {\n            border-bottom: 1px solid var(--color-border);\n        }\n    }\n\n    .action {\n        display: flex;\n        flex-wrap: wrap;\n        padding: 5px;\n\n        .action-item {\n            width: 33.3333%;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/FastPanel/FastPanelSearch.vue",
    "content": "<script setup lang=\"ts\">\nimport IconFormatText from \"~icons/mdi/format-text\";\nimport { useDragWindow } from \"../../app/dragWindow\";\nimport FileExt from \"../../components/common/FileExt.vue\";\nimport { useManagerStore } from \"../../store/modules/manager\";\nimport { EntryListener } from \"../Main/Lib/entryListener\";\nimport { useSearchOperate } from \"../Main/Lib/searchOperate\";\n\nconst emit = defineEmits([]);\nconst manager = useManagerStore();\n\nconst { clipboardFilesInfo } = useSearchOperate(emit);\n\nconst { onDragWindowMouseDown } = useDragWindow({\n    name: \"fastPanel\",\n});\n\nconst onShow = () => {\n    EntryListener.prepareSearch({\n        isFastPanel: true,\n    }).then();\n};\n\ndefineExpose({\n    onShow,\n});\n</script>\n\n<template>\n    <div class=\"pb-search\" @click=\"onShow\" @mousedown=\"onDragWindowMouseDown\">\n        <div class=\"left\">\n            <div v-if=\"manager.currentFiles.length > 0\" class=\"file\">\n                <div class=\"type\">\n                    <FileExt :name=\"clipboardFilesInfo.extName\" />\n                </div>\n                <div class=\"title\">{{ clipboardFilesInfo.name }}</div>\n                <div class=\"count\" v-if=\"manager.currentFiles.length > 1\">\n                    x{{ manager.currentFiles.length }}\n                </div>\n            </div>\n            <div v-else-if=\"manager.currentImage\" class=\"image\">\n                <img :src=\"manager.currentImage\" />\n            </div>\n            <div v-else-if=\"manager.currentText\" class=\"text\">\n                <IconFormatText />\n                <div class=\"content\">\n                    {{ manager.currentText }}\n                </div>\n            </div>\n            <div v-else>{{ $t(\"fastPanel.shortcuts\") }}</div>\n        </div>\n        <div class=\"right\">\n            <div class=\"icon\" @click=\"manager.showMainWindow()\">\n                <img src=\"./../../assets/image/logo.svg\" />\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped lang=\"less\">\n.pb-search {\n    display: flex;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    align-items: center;\n    height: 40px;\n    padding: 10px;\n    background-color: var(--color-background);\n    border-bottom: 1px solid var(--color-border);\n    z-index: 10;\n    user-select: none;\n\n    .left {\n        display: flex;\n        align-items: center;\n        flex-grow: 1;\n        border-radius: 10px;\n        height: 40px;\n        width: 0;\n\n        .text {\n            height: 30px;\n            line-height: 30px;\n            padding: 0 10px;\n            margin-right: 5px;\n            border-radius: 5px;\n            box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);\n            background: var(--color-background-content);\n            max-width: 10rem;\n            display: flex;\n            align-items: center;\n            flex-wrap: nowrap;\n\n            svg {\n                flex-shrink: 0;\n                width: 20px;\n                height: 20px;\n                line-height: 20px;\n                display: block;\n            }\n\n            .content {\n                overflow: hidden;\n                text-overflow: ellipsis;\n                white-space: nowrap;\n            }\n        }\n\n        .image {\n            margin-right: 5px;\n\n            img {\n                max-height: 30px;\n                max-width: 60px;\n                border-radius: 5px;\n                box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);\n                background-color: var(--color-background);\n            }\n        }\n\n        .file {\n            display: flex;\n            align-items: center;\n            margin-right: 5px;\n            border-radius: 5px;\n            box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);\n            padding: 0 10px;\n            height: 30px;\n            background-color: var(--color-background);\n            max-width: 90%;\n\n            .type {\n                width: 20px;\n                height: 20px;\n                margin-right: 5px;\n            }\n\n            .title {\n                line-height: 20px;\n                color: #333;\n                font-weight: bold;\n                white-space: nowrap;\n                overflow: hidden;\n                text-overflow: ellipsis;\n                flex-grow: 1;\n            }\n\n            .count {\n                line-height: 20px;\n                color: #fff;\n                background: #cf0707;\n                border-radius: 10px;\n                padding: 0 5px;\n                margin-left: 5px;\n                font-size: 12px;\n            }\n        }\n    }\n\n    .right {\n        display: flex;\n        align-items: center;\n        width: 40px;\n        flex-shrink: 0;\n        justify-content: flex-end;\n\n        .icon {\n            cursor: pointer;\n\n            &:hover {\n                img {\n                    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\n                }\n            }\n\n            img {\n                width: 30px;\n                height: 30px;\n                border-radius: 50%;\n                transition: box-shadow 0.5s;\n                object-fit: contain;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/FastPanel/Lib/resultOperate.ts",
    "content": "import { useManagerStore } from \"../../../store/modules/manager\";\nimport { ActionRecord } from \"../../../types/Manager\";\n\nconst manager = useManagerStore();\n\nexport const useResultOperate = () => {\n    const doOpenAction = async (action: ActionRecord) => {\n        // await manager.showMainWindow()\n        await manager.openAction(action);\n    };\n\n    return {\n        doOpenAction,\n    };\n};\n"
  },
  {
    "path": "src/pages/Home.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeMount, onMounted, ref } from \"vue\";\nimport { AppConfig } from \"../config\";\n\nconst loading = ref(true);\n\nonMounted(async () => {\n    loading.value = false;\n});\n\nonBeforeMount(() => {});\n</script>\n\n<template>\n    <div class=\"page-narrow-container p-8\">\n        <div class=\"text-3xl font-bold mb-4\">\n            {{ $t(\"home.welcome\") }} {{ AppConfig.name }} ！\n        </div>\n        <div></div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/Main/Components/ResultActionCodeError.vue",
    "content": "<script setup lang=\"ts\">\nconst props = defineProps<{\n    error: string;\n}>();\n</script>\n<template>\n    <div>\n        <div class=\"text-center py-12\">\n            <div class=\"mb-6 relative\">\n                <div\n                    class=\"w-12 h-12 bg-gradient-to-b from-gray-100 to-gray-300 absolute top-0 left-0 right-0 bottom-0 m-auto rounded-full animate-spin\"\n                ></div>\n                <img\n                    class=\"w-10 h-10 opacity-50 mx-auto\"\n                    src=\"./../../../assets/image/search-icon.svg\"\n                />\n            </div>\n            <div class=\"text-red-500\">\n                {{ $t(\"main.runError\") }}\n            </div>\n            <div>\n                {{ error }}\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/Main/Components/ResultActionCodeItemList.vue",
    "content": "<script setup lang=\"ts\">\nimport { useManagerStore } from \"../../../store/modules/manager\";\n\nconst manager = useManagerStore();\nconst props = defineProps<{\n    isOsx: boolean;\n    doOpenActionCode: (id: string) => void;\n}>();\n</script>\n\n<template>\n    <div>\n        <div\n            class=\"pb-main-result-code-item flex items-center p-2 border-t border-gray-300 hover:bg-gray-100\"\n            v-for=\"(ci, ciIndex) in manager.actionCodeItems\"\n            :id=\"`MainResult_CodeItem_${ci.id}`\"\n            @click=\"doOpenActionCode(ci.id)\"\n            :class=\"{ 'bg-gray-200': manager.actionCodeItemActiveId === ci.id }\"\n        >\n            <div\n                class=\"w-10 h-10 flex items-center justify-center border border-gray-300 mr-3 rounded-lg\"\n            >\n                <div\n                    class=\"w-8 h-8 bg-contain bg-center bg-no-repeat\"\n                    :style=\"{ backgroundImage: `url(${ci.icon})` }\"\n                ></div>\n            </div>\n            <div class=\"flex-grow\">\n                <div>{{ ci.title }}</div>\n                <div class=\"text-sm text-gray-500\">{{ ci.description }}</div>\n            </div>\n            <div v-if=\"ci.shortcutIndex > 0\">\n                <div\n                    v-if=\"isOsx\"\n                    class=\"text-xs bg-gray-100 tw-inline-block px-2 py-1 rounded-lg font-mono\"\n                >\n                    <icon-command />\n                    +{{ ci.shortcutIndex }}\n                </div>\n                <div\n                    v-else\n                    class=\"text-xs bg-gray-100 tw-inline-block px-2 py-1 rounded-lg font-mono\"\n                >\n                    CTRL+{{ ci.shortcutIndex }}\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/Main/Components/ResultActionCodeLoading.vue",
    "content": "<template>\n    <div>\n        <div class=\"text-center py-12\">\n            <div class=\"mb-6 relative\">\n                <div\n                    class=\"w-12 h-12 bg-gradient-to-b from-gray-100 to-gray-300 absolute top-0 left-0 right-0 bottom-0 m-auto rounded-full animate-spin\"\n                ></div>\n                <img\n                    class=\"w-10 h-10 opacity-50 mx-auto\"\n                    src=\"./../../../assets/image/search-icon.svg\"\n                />\n            </div>\n            <div class=\"text-gray-400\">{{ $t(\"main.loading\") }}</div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/Main/Components/ResultItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { PropType } from \"vue\";\nimport IconPin from \"~icons/mdi/pin\";\nimport { ActionRecord, PluginType } from \"../../../types/Manager\";\n\nconst emit = defineEmits([\"open\", \"delete\", \"pin\"]);\nconst props = defineProps({\n    action: {\n        type: Object as PropType<ActionRecord>,\n        required: true,\n    },\n    showDelete: {\n        type: Boolean,\n        default: false,\n    },\n    showPin: {\n        type: Boolean,\n        default: false,\n    },\n});\n</script>\n\n<template>\n    <div\n        class=\"item-box hover:bg-gray-100 dark:hover:bg-gray-700\"\n        :data-action=\"action.fullName\"\n    >\n        <div class=\"icon\" @click=\"emit('open')\">\n            <img\n                draggable=\"false\"\n                :class=\"\n                    action.pluginType === PluginType.SYSTEM\n                        ? 'dark:invert'\n                        : 'plugin-logo-filter'\n                \"\n                :src=\"action.icon\"\n            />\n        </div>\n        <div class=\"title\" @click=\"emit('open')\">\n            <span\n                v-if=\"action.runtime?.searchTitleMatched\"\n                v-html=\"action.runtime?.searchTitleMatched\"\n            ></span>\n            <span v-else>{{ action.title }}</span>\n            <div v-if=\"0\" class=\"absolute left-0 top-0\" style=\"font-size: 8px\">\n                {{ action.fullName }}\n            </div>\n        </div>\n        <div class=\"action\" v-if=\"showDelete || showPin\">\n            <a href=\"javascript:;\" v-if=\"showDelete\" @click=\"emit('delete')\">\n                <icon-delete />\n            </a>\n            <a href=\"javascript:;\" v-if=\"showPin\" @click=\"emit('pin')\">\n                <IconPin />\n            </a>\n        </div>\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n.item-box {\n    height: 96px;\n    border-radius: 10px;\n    padding-top: 12px;\n    position: relative;\n\n    &:hover {\n        .action {\n            display: block;\n        }\n    }\n\n    .action {\n        position: absolute;\n        top: 0;\n        right: 0;\n        cursor: default;\n        display: none;\n\n        a {\n            display: inline-block;\n            width: 20px;\n            height: 20px;\n            line-height: 20px;\n            text-align: center;\n            color: #999;\n\n            &:hover {\n                color: var(--color-primary);\n            }\n        }\n    }\n\n    :deep(mark) {\n        color: red;\n        background: none;\n    }\n\n    .icon {\n        text-align: center;\n        cursor: pointer;\n\n        img {\n            height: 40px;\n            width: 40px;\n            margin: 0 auto;\n        }\n    }\n\n    .title {\n        margin-top: 5px;\n        font-size: 14px;\n        line-height: 16px;\n        text-align: center;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        padding: 0 5px;\n        display: -webkit-box;\n        -webkit-box-orient: vertical;\n        -webkit-line-clamp: 2;\n        cursor: pointer;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/Main/Components/ResultLoading.vue",
    "content": "<script setup lang=\"ts\"></script>\n\n<template>\n    <div>\n        <div class=\"text-center py-32\">\n            <div class=\"mb-6 relative\">\n                <div\n                    class=\"w-12 h-12 bg-gradient-to-b from-gray-100 to-gray-300 absolute top-0 left-0 right-0 bottom-0 m-auto rounded-full animate-spin\"\n                ></div>\n                <img\n                    class=\"w-10 h-10 opacity-50 mx-auto\"\n                    src=\"./../../../assets/image/search-icon.svg\"\n                />\n            </div>\n            <div class=\"text-gray-400\">{{ $t(\"main.starting\") }}</div>\n        </div>\n    </div>\n</template>\n\n<style scoped lang=\"scss\"></style>\n"
  },
  {
    "path": "src/pages/Main/Components/ResultWindowItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { PropType } from \"vue\";\nimport IconPin from \"~icons/mdi/pin\";\nimport { ActionRecord, PluginType } from \"../../../types/Manager\";\n\nconst emit = defineEmits([\"open\", \"delete\", \"pin\"]);\nconst props = defineProps({\n    action: {\n        type: Object as PropType<ActionRecord>,\n        required: true,\n    },\n    showDelete: {\n        type: Boolean,\n        default: false,\n    },\n    showPin: {\n        type: Boolean,\n        default: false,\n    },\n});\n</script>\n\n<template>\n    <div\n        class=\"item-window-box hover:bg-gray-100 dark:hover:bg-gray-700\"\n        :data-action=\"action.fullName\"\n    >\n        <div class=\"icon\" @click=\"emit('open')\">\n            <img\n                draggable=\"false\"\n                :class=\"\n                    action.pluginType === PluginType.SYSTEM\n                        ? 'dark:invert'\n                        : 'plugin-logo-filter'\n                \"\n                :src=\"action.icon\"\n            />\n        </div>\n        <div class=\"title\" @click=\"emit('open')\">\n            <span\n                v-if=\"action.runtime?.searchTitleMatched\"\n                v-html=\"action.runtime?.searchTitleMatched\"\n            ></span>\n            <span v-else>{{ action.title }}</span>\n        </div>\n        <div\n            class=\"index\"\n            v-if=\"\n                action.runtime?.windowCount && action.runtime?.windowCount > 1\n            \"\n        >\n            {{ action.runtime?.windowIndex }}\n        </div>\n        <div class=\"action\" v-if=\"showDelete || showPin\">\n            <a href=\"javascript:;\" v-if=\"showDelete\" @click=\"emit('delete')\">\n                <icon-delete />\n            </a>\n            <a href=\"javascript:;\" v-if=\"showPin\" @click=\"emit('pin')\">\n                <IconPin />\n            </a>\n        </div>\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n.item-window-box {\n    height: 90px;\n    border-radius: 10px;\n    position: relative;\n    padding-top: 4px;\n    border: 2px solid #eee;\n    border-top-width: 8px;\n    margin-right: 5px;\n\n    &:hover {\n        .action {\n            display: block;\n        }\n    }\n\n    .action {\n        position: absolute;\n        top: 0;\n        right: 0;\n        cursor: default;\n        display: none;\n\n        a {\n            display: inline-block;\n            width: 20px;\n            height: 20px;\n            line-height: 20px;\n            text-align: center;\n            color: #999;\n\n            &:hover {\n                color: var(--color-primary);\n            }\n        }\n    }\n\n    .index {\n        position: absolute;\n        top: 2px;\n        right: 2px;\n        width: 20px;\n        height: 20px;\n        line-height: 20px;\n        text-align: center;\n        color: #fff;\n        background-color: #999;\n        font-size: 12px;\n        border-radius: 50%;\n    }\n\n    :deep(mark) {\n        color: red;\n        background: none;\n    }\n\n    .icon {\n        text-align: center;\n        cursor: pointer;\n\n        img {\n            height: 40px;\n            width: 40px;\n            margin: 0 auto;\n        }\n    }\n\n    .title {\n        margin-top: 5px;\n        font-size: 14px;\n        line-height: 16px;\n        text-align: center;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        padding: 0 5px;\n        display: -webkit-box;\n        -webkit-box-orient: vertical;\n        -webkit-line-clamp: 2;\n        cursor: pointer;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/Main/Lib/entryListener.ts",
    "content": "import { useManagerStore } from \"../../../store/modules/manager\";\nimport { ClipboardDataType, SelectedContent } from \"../../../types/Manager\";\nimport { TimeUtil } from \"../../../lib/util\";\n\nconst manager = useManagerStore();\n\nexport const EntryListener = {\n    prepareSearch: async (option: {\n        // 主动粘贴\n        isPaste?: boolean;\n        // 快速面板\n        isFastPanel?: boolean;\n    }) => {\n        // console.log('EntryListener.prepareSearch', option)\n        option = Object.assign(\n            {\n                isPaste: false,\n                isFastPanel: false,\n            },\n            option,\n        );\n        // console.log('EntryListener.prepareSearch', option)\n\n        let searchValue = manager.searchValue;\n\n        let selectedContent: SelectedContent | null = null;\n\n        // the fast panel should check the selected content\n        if (option.isFastPanel) {\n            selectedContent = await window.$mapi.manager.getSelectedContent();\n        }\n\n        const clipboardContent: ClipboardDataType | null =\n            await window.$mapi.manager.getClipboardContent();\n\n        let useClipboard = false;\n        // first use clipboard\n        if (manager.showFirstRun) {\n            manager.showFirstRun = false;\n            const clipboardChangeTime =\n                await window.$mapi.manager.getClipboardChangeTime();\n            // only use clipboard if it has changed in the last 3 seconds\n            if (\n                clipboardChangeTime > 0 &&\n                clipboardChangeTime > TimeUtil.timestamp() - 3\n            ) {\n                useClipboard = true;\n            }\n        }\n        if (!useClipboard && option.isPaste) {\n            useClipboard = true;\n        }\n\n        // files\n        manager.setCurrentFiles([]);\n        if (\n            selectedContent &&\n            selectedContent.type === \"file\" &&\n            selectedContent.files?.length\n        ) {\n            manager.setCurrentFiles(selectedContent.files as FileItem[]);\n        } else if (\n            useClipboard &&\n            clipboardContent &&\n            clipboardContent.type === \"file\" &&\n            clipboardContent.files?.length\n        ) {\n            manager.setCurrentFiles(clipboardContent.files as FileItem[]);\n        }\n\n        // image\n        manager.setCurrentImage(\"\");\n        if (\n            useClipboard &&\n            clipboardContent &&\n            clipboardContent.type === \"image\" &&\n            clipboardContent.image\n        ) {\n            manager.setCurrentImage(clipboardContent.image);\n        }\n\n        // text\n        manager.setCurrentText(\"\");\n        if (\n            selectedContent &&\n            selectedContent.type === \"text\" &&\n            selectedContent.text\n        ) {\n            manager.setCurrentText(selectedContent.text);\n        } else if (\n            useClipboard &&\n            clipboardContent &&\n            clipboardContent.type === \"text\" &&\n            clipboardContent.text\n        ) {\n            manager.setCurrentText(clipboardContent.text);\n        }\n        if (!option.isFastPanel && manager.currentText) {\n            if (\n                manager.currentText.split(\"\\n\").length === 1 &&\n                manager.currentText.length < 100\n            ) {\n                if (\n                    !manager.searchLastKeywords ||\n                    (manager.searchLastKeywords &&\n                        manager.searchLastKeywords !== manager.currentText)\n                ) {\n                    searchValue = manager.currentText;\n                    manager.setCurrentText(\"\");\n                }\n            }\n        }\n\n        if (option.isFastPanel) {\n            await manager.searchFastPanel(searchValue);\n        } else {\n            await manager.search(searchValue);\n        }\n\n        // console.log('state', JSON.stringify({\n        //     searchValue,\n        //     option,\n        //     useClipboard,\n        //     clipboardContent,\n        //     image: manager.currentImage,\n        //     files: manager.currentFiles,\n        //     text: manager.currentText\n        // }, null, 2))\n    },\n};\n"
  },
  {
    "path": "src/pages/Main/Lib/mainOperate.ts",
    "content": "import { useManagerStore } from \"../../../store/modules/manager\";\nimport { computed } from \"vue\";\n\nconst manager = useManagerStore();\nexport const useMainOperate = () => {\n    const hasActions = computed(() => {\n        return (\n            manager.searchActions.length > 0 ||\n            manager.matchActions.length > 0 ||\n            manager.historyActions.length > 0 ||\n            manager.pinActions.length > 0\n        );\n    });\n\n    let detachHotKey: any = null;\n    let detachHotkeyExpire = 0;\n    let detachHotkeyTimes = 0;\n\n    const onKeyDown = (e: KeyboardEvent) => {\n        let resultKey = \"\";\n\n        const { ctrlKey, shiftKey, altKey, metaKey } = e;\n\n        const modifiers: Array<string> = [];\n        ctrlKey && modifiers.push(\"control\");\n        shiftKey && modifiers.push(\"shift\");\n        altKey && modifiers.push(\"alt\");\n        metaKey && modifiers.push(\"meta\");\n\n        if (!detachHotKey) {\n            detachHotKey = manager.configGet(\"detachWindowTrigger\", null);\n        }\n        // console.log('keydown', e)\n        // {\"key\":\"D\",\"altKey\":false,\"ctrlKey\":false,\"metaKey\":true,\"shiftKey\":false,\"times\":1}\n        if (detachHotKey && detachHotKey.value) {\n            // console.log('detachHotkeyExpire', detachHotKey.value.key, detachHotkeyExpire)\n            if (\n                detachHotKey.value.key === e.key.toUpperCase() &&\n                detachHotKey.value.altKey === altKey &&\n                detachHotKey.value.ctrlKey === ctrlKey &&\n                detachHotKey.value.metaKey === metaKey &&\n                detachHotKey.value.shiftKey === shiftKey\n            ) {\n                if (!detachHotkeyExpire || Date.now() > detachHotkeyExpire) {\n                    detachHotkeyExpire = Date.now() + 500;\n                    detachHotkeyTimes = 1;\n                } else {\n                    detachHotkeyTimes++;\n                }\n                if (detachHotkeyTimes >= detachHotKey.value.times) {\n                    detachHotkeyExpire = 0;\n                    detachHotkeyTimes = 0;\n                    manager.detachPlugin();\n                    return {\n                        resultKey,\n                    };\n                }\n            }\n        }\n        const map = {\n            Escape: \"esc\",\n            ArrowLeft: \"left\",\n            ArrowRight: \"right\",\n            ArrowDown: \"down\",\n            ArrowUp: \"up\",\n            Enter: \"enter\",\n            \" \": \"space\",\n        };\n        const key = map[e.key] || \"custom\";\n        switch (key) {\n            case \"up\":\n            case \"down\":\n            case \"left\":\n            case \"right\":\n            case \"enter\":\n                if (hasActions.value) {\n                    resultKey = key;\n                }\n                break;\n            case \"esc\":\n                if (manager.activePlugin) {\n                    manager.closeMainPlugin().then();\n                } else {\n                    manager.hideMainWindow().then();\n                }\n                break;\n            default:\n                switch (e.keyCode) {\n                    case 8:\n                        if (manager.searchValue === \"\") {\n                            resultKey = \"delete\";\n                        }\n                        break;\n                    case 86:\n                        if (manager.searchValue === \"\") {\n                            if (ctrlKey || metaKey) {\n                                resultKey = \"paste\";\n                            }\n                        }\n                        break;\n                }\n                break;\n        }\n        if (resultKey) {\n            e.preventDefault();\n        }\n        return {\n            resultKey,\n        };\n    };\n\n    return {\n        onKeyDown,\n    };\n};\n"
  },
  {
    "path": "src/pages/Main/Lib/resultOperate.ts",
    "content": "import { ComputedRef } from \"@vue/reactivity\";\nimport { chunk } from \"lodash-es\";\nimport { computed, ref, watch } from \"vue\";\nimport { t } from \"../../../lang\";\nimport { Dialog } from \"../../../lib/dialog\";\nimport { UI } from \"../../../lib/ui\";\nimport { useManagerStore } from \"../../../store/modules/manager\";\nimport { ActionRecord } from \"../../../types/Manager\";\nimport { EntryListener } from \"./entryListener\";\n\ntype ActionGroupType =\n    | \"window\"\n    | \"search\"\n    | \"match\"\n    | \"history\"\n    | \"pin\"\n    | never;\n\nconst manager = useManagerStore();\n\nexport const useResultOperate = () => {\n    const hasActions = computed(() => {\n        return (\n            manager.detachWindowActions.length > 0 ||\n            manager.searchActions.length > 0 ||\n            manager.matchActions.length > 0 ||\n            manager.historyActions.length > 0 ||\n            manager.pinActions.length > 0\n        );\n    });\n    const hasViewActions = computed(() => {\n        return manager.viewActions.length > 0;\n    });\n    const lineActionCount = computed(() => {\n        return manager.viewActions.length > 0 ? 5 : 8;\n    });\n\n    const searchActionIsExtend = ref<Boolean>(false);\n    const matchActionIsExtend = ref<Boolean>(false);\n    const historyActionIsExtend = ref<Boolean>(false);\n    const pinActionIsExtend = ref<Boolean>(false);\n\n    watch(\n        () => manager.searchActions,\n        () => {\n            searchActionIsExtend.value =\n                manager.searchActions.length <= lineActionCount.value;\n            resetActive();\n        },\n    );\n    watch(\n        () => manager.matchActions,\n        () => {\n            matchActionIsExtend.value =\n                manager.matchActions.length <= lineActionCount.value;\n            resetActive();\n        },\n    );\n    watch(\n        () => manager.historyActions,\n        () => {\n            historyActionIsExtend.value =\n                manager.historyActions.length <= lineActionCount.value;\n            resetActive();\n        },\n    );\n    watch(\n        () => manager.pinActions,\n        () => {\n            pinActionIsExtend.value =\n                manager.pinActions.length <= lineActionCount.value;\n            resetActive();\n        },\n    );\n\n    const doSearchActionExtend = () => {\n        if (searchActionIsExtend.value) {\n            return;\n        }\n        searchActionIsExtend.value = true;\n    };\n    const doMatchActionExtend = () => {\n        if (matchActionIsExtend.value) {\n            return;\n        }\n        matchActionIsExtend.value = true;\n    };\n    const doHistoryActionExtend = () => {\n        if (historyActionIsExtend.value) {\n            return;\n        }\n        historyActionIsExtend.value = true;\n    };\n    const doPinActionExtend = () => {\n        if (pinActionIsExtend.value) {\n            return;\n        }\n        pinActionIsExtend.value = true;\n    };\n\n    const showDetachWindowActions: ComputedRef<ActionRecord[]> = computed(\n        () => {\n            return manager.detachWindowActions;\n        },\n    );\n    const showSearchActions: ComputedRef<ActionRecord[]> = computed(() => {\n        return searchActionIsExtend.value\n            ? manager.searchActions\n            : manager.searchActions.slice(0, lineActionCount.value);\n    });\n    const showMatchActions: ComputedRef<ActionRecord[]> = computed(() => {\n        return matchActionIsExtend.value\n            ? manager.matchActions\n            : manager.matchActions.slice(0, lineActionCount.value);\n    });\n    const showHistoryActions: ComputedRef<ActionRecord[]> = computed(() => {\n        return historyActionIsExtend.value\n            ? manager.historyActions\n            : manager.historyActions.slice(0, lineActionCount.value);\n    });\n    const showPinActions: ComputedRef<ActionRecord[]> = computed(() => {\n        return pinActionIsExtend.value\n            ? manager.pinActions\n            : manager.pinActions.slice(0, lineActionCount.value);\n    });\n\n    const activeActionGroup = ref<ActionGroupType>(\"search\");\n    const actionActionIndex = ref<number>(0);\n    const resetActive = () => {\n        if (manager.detachWindowActions.length > 0) {\n            activeActionGroup.value = \"window\";\n        } else if (manager.searchActions.length > 0) {\n            activeActionGroup.value = \"search\";\n        } else if (manager.matchActions.length > 0) {\n            activeActionGroup.value = \"match\";\n        } else if (manager.historyActions.length > 0) {\n            activeActionGroup.value = \"history\";\n        } else if (manager.pinActions.length > 0) {\n            activeActionGroup.value = \"pin\";\n        }\n        actionActionIndex.value = 0;\n    };\n\n    const doCodeNavigate = (direction: string) => {\n        let index = manager.actionCodeItems.findIndex(\n            (item) => item.id === manager.actionCodeItemActiveId,\n        );\n        switch (direction) {\n            case \"up\":\n            case \"left\":\n                index = Math.max(index - 1, 0);\n                break;\n            case \"down\":\n            case \"right\":\n                index = Math.min(index + 1, manager.actionCodeItems.length - 1);\n                break;\n        }\n        manager.actionCodeItemActiveId = manager.actionCodeItems[index].id;\n        setTimeout(() => {\n            const codeItemElement = document.getElementById(\n                `MainResult_CodeItem_${manager.actionCodeItemActiveId}`,\n            );\n            if (codeItemElement) {\n                const container = document.getElementById(\n                    \"MainResult_Container\",\n                );\n                if (container) {\n                    UI.smoothScrollTop(\n                        container,\n                        codeItemElement.offsetTop -\n                            container.offsetTop -\n                            container.clientHeight / 2 +\n                            codeItemElement.clientHeight / 2,\n                    ).then(() => {\n                        // 计算完全在可视范围内的元素，使用shortcutIndex进行编号\n                        const visibleItemIndexes = Array.from(\n                            container.querySelectorAll(\n                                \".pb-main-result-code-item\",\n                            ),\n                        )\n                            .map((el, idx) => {\n                                const item = el as HTMLElement;\n                                const itemTop =\n                                    item.offsetTop - container.offsetTop;\n                                const itemBottom = itemTop + item.clientHeight;\n                                if (\n                                    itemTop >= container.scrollTop &&\n                                    itemBottom <=\n                                        container.scrollTop +\n                                            container.clientHeight\n                                ) {\n                                    return idx;\n                                }\n                                return null;\n                            })\n                            .filter((idx) => idx !== null) as number[];\n                        manager.actionCodeItems.forEach((item, idx) => {\n                            if (visibleItemIndexes.includes(idx)) {\n                                item.shortcutIndex =\n                                    visibleItemIndexes.indexOf(idx) + 1;\n                            } else {\n                                item.shortcutIndex = -1;\n                            }\n                        });\n                    });\n                }\n            }\n        }, 10);\n    };\n\n    const _doActionNavigate = (direction: string) => {\n        const grids: any[][] = [];\n        [\n            [showDetachWindowActions.value, \"window\"],\n            [showSearchActions.value, \"search\"],\n            [showMatchActions.value, \"match\"],\n            [showHistoryActions.value, \"history\"],\n            [showPinActions.value, \"pin\"],\n        ].forEach((actions) => {\n            let items = [] as any[];\n            (actions[0] as ActionRecord[]).forEach((_, itemIndex) => {\n                items.push({\n                    group: actions[1],\n                    index: itemIndex,\n                });\n            });\n            chunk(items, lineActionCount.value).forEach((chunk) => {\n                grids.push(chunk);\n            });\n        });\n        let activeGridRowIndex = grids.findIndex((gridLine) =>\n            gridLine.find(\n                (grid) =>\n                    grid.group === activeActionGroup.value &&\n                    grid.index === actionActionIndex.value,\n            ),\n        );\n        let activeGridColIndex = grids[activeGridRowIndex].findIndex(\n            (grid) =>\n                grid.group === activeActionGroup.value &&\n                grid.index === actionActionIndex.value,\n        );\n        switch (direction) {\n            case \"up\":\n                if (activeGridRowIndex > 0) {\n                    activeGridRowIndex--;\n                    activeGridColIndex = Math.min(\n                        activeGridColIndex,\n                        grids[activeGridRowIndex].length - 1,\n                    );\n                }\n                break;\n            case \"down\":\n                if (activeGridRowIndex < grids.length - 1) {\n                    activeGridRowIndex++;\n                    activeGridColIndex = Math.min(\n                        activeGridColIndex,\n                        grids[activeGridRowIndex].length - 1,\n                    );\n                }\n                break;\n            case \"left\":\n                activeGridColIndex--;\n                if (activeGridColIndex < 0) {\n                    if (activeGridRowIndex > 0) {\n                        activeGridRowIndex--;\n                        activeGridColIndex =\n                            grids[activeGridRowIndex].length - 1;\n                    } else {\n                        activeGridColIndex = 0;\n                    }\n                }\n                break;\n            case \"right\":\n                activeGridColIndex++;\n                if (activeGridColIndex >= grids[activeGridRowIndex].length) {\n                    if (activeGridRowIndex < grids.length - 1) {\n                        activeGridRowIndex++;\n                        activeGridColIndex = 0;\n                    } else {\n                        activeGridColIndex =\n                            grids[activeGridRowIndex].length - 1;\n                    }\n                }\n                break;\n        }\n        activeActionGroup.value =\n            grids[activeGridRowIndex][activeGridColIndex].group;\n        actionActionIndex.value =\n            grids[activeGridRowIndex][activeGridColIndex].index;\n        manager.setSelectedAction(_getActiveAction() as ActionRecord);\n    };\n\n    const _getActiveAction = () => {\n        let activeAction: any = null;\n        switch (activeActionGroup.value) {\n            case \"window\":\n                activeAction =\n                    showDetachWindowActions.value[actionActionIndex.value];\n                break;\n            case \"search\":\n                activeAction = showSearchActions.value[actionActionIndex.value];\n                break;\n            case \"match\":\n                activeAction = showMatchActions.value[actionActionIndex.value];\n                break;\n            case \"history\":\n                activeAction =\n                    showHistoryActions.value[actionActionIndex.value];\n                break;\n            case \"pin\":\n                activeAction = showPinActions.value[actionActionIndex.value];\n                break;\n        }\n        return activeAction as ActionRecord | null;\n    };\n\n    const onInputKey = (key: string) => {\n        if ([\"up\", \"down\", \"left\", \"right\"].includes(key)) {\n            if (manager.activePlugin && manager.activePluginType === \"code\") {\n                doCodeNavigate(key);\n            } else {\n                _doActionNavigate(key);\n            }\n        } else if (\"enter\" === key) {\n            if (manager.activePlugin && manager.activePluginType === \"code\") {\n                if (manager.actionCodeItemActiveId) {\n                    doOpenActionCode(manager.actionCodeItemActiveId).then();\n                }\n                return;\n            }\n            if (manager.searchIsCompositing) {\n                return;\n            }\n            const action = _getActiveAction();\n            if (action) {\n                if (activeActionGroup.value === \"window\") {\n                    openActionWindow(\"open\", action).then();\n                } else {\n                    doOpenAction(action).then();\n                }\n            }\n        } else if (\"delete\" === key) {\n            if (\"\" === manager.searchValue) {\n                onClose();\n            }\n        } else if (\"paste\" === key) {\n            if (!manager.activePlugin) {\n                EntryListener.prepareSearch({ isPaste: true }).then();\n            }\n        }\n    };\n\n    const onClose = () => {\n        if (manager.activePlugin) {\n            doClosePlugin().then();\n        } else {\n            manager.setCurrentFiles([]);\n            manager.setCurrentImage(\"\");\n            manager.setCurrentText(\"\");\n            manager.search(\"\").then();\n        }\n    };\n\n    const doClosePlugin = async () => {\n        await manager.closeMainPlugin();\n    };\n\n    const doOpenAction = async (action: ActionRecord) => {\n        await manager.openAction(action);\n    };\n\n    const doOpenActionCode = async (id: string) => {\n        await manager.openActionCode(id);\n    };\n\n    const openActionWindow = async (type: \"open\", action: ActionRecord) => {\n        await manager.openActionWindow(type, action);\n    };\n\n    const doHistoryClear = async () => {\n        Dialog.confirm(t(\"main.clearAllConfirm\")).then(() => {\n            window.$mapi.manager.historyClear();\n            manager.searchRefresh().then();\n        });\n    };\n\n    const doHistoryDelete = async (action: ActionRecord) => {\n        await window.$mapi.manager.historyDelete(\n            action.pluginName as string,\n            action.name,\n        );\n        await manager.searchRefresh();\n    };\n\n    const doPinToggle = async (action: ActionRecord) => {\n        await window.$mapi.manager.togglePinAction(\n            action.pluginName as string,\n            action.name,\n        );\n        await manager.searchRefresh();\n    };\n\n    return {\n        hasActions,\n        hasViewActions,\n        searchActionIsExtend,\n        matchActionIsExtend,\n        historyActionIsExtend,\n        pinActionIsExtend,\n        doSearchActionExtend,\n        doMatchActionExtend,\n        doHistoryActionExtend,\n        doPinActionExtend,\n        showDetachWindowActions,\n        showSearchActions,\n        showMatchActions,\n        showHistoryActions,\n        showPinActions,\n        activeActionGroup,\n        actionActionIndex,\n        onInputKey,\n        onClose,\n        doOpenAction,\n        doOpenActionCode,\n        openActionWindow,\n        doHistoryClear,\n        doHistoryDelete,\n        doPinToggle,\n    };\n};\n"
  },
  {
    "path": "src/pages/Main/Lib/resultResize.ts",
    "content": "import { onBeforeUnmount, onMounted } from \"vue\";\nimport { UI } from \"../../../lib/ui\";\nimport { useManagerStore } from \"../../../store/modules/manager\";\nimport { WindowConfig } from \"../../../../electron/config/window\";\n\nconst manager = useManagerStore();\n\nlet ignoreNextResize = false;\nexport const ignoreNextResultResize = () => {\n    ignoreNextResize = true;\n};\n\nexport const useResultResize = (groupContainer: any) => {\n    onMounted(() => {\n        UI.onResize(groupContainer.value, (width: number, height: number) => {\n            // console.log('resize', width, height, manager.activePlugin)\n            if (!manager.activePlugin && !ignoreNextResize) {\n                manager.resize(width, height + WindowConfig.mainHeight).then();\n            } else if (\n                manager.activePlugin &&\n                manager.activePluginType === \"code\"\n            ) {\n                manager.resize(width, height + WindowConfig.mainHeight).then();\n            }\n        });\n    });\n    onBeforeUnmount(() => {\n        UI.offResize(groupContainer.value);\n    });\n};\n\nexport const fireResultResize = (groupContainer: any) => {\n    // console.log('fireResultResize', groupContainer.value)\n    UI.fireResize(groupContainer.value);\n};\n"
  },
  {
    "path": "src/pages/Main/Lib/searchOperate.ts",
    "content": "import { computed } from \"vue\";\nimport { t } from \"../../../lang\";\nimport { useManagerStore } from \"../../../store/modules/manager\";\n\nconst { Menu } = require(\"@electron/remote\");\n\nconst manager = useManagerStore();\n\nexport const useSearchOperate = (emit) => {\n    const doShowMenu = () => {\n        const menuTemplate: any[] = [];\n        if (manager.activePlugin) {\n            if (manager.activePluginType === \"code\") {\n                // do nothing\n            } else {\n                menuTemplate.push({\n                    label: t(\"plugin.detachWindow\"),\n                    click: () => {\n                        doDetachPlugin().then();\n                    },\n                });\n            }\n            menuTemplate.push({\n                label: t(\"plugin.debugWindow\"),\n                click: async () => {\n                    manager.openMainPluginDevTools().then();\n                },\n            });\n            menuTemplate.push({\n                label: t(\"plugin.backendLog\"),\n                click: async () => {\n                    manager.openMainPluginLog().then();\n                },\n            });\n            if (manager.activePlugin.setting) {\n                if (\n                    manager.activePlugin.setting.moreMenu &&\n                    manager.activePlugin.setting.moreMenu.length > 0\n                ) {\n                    for (const item of manager.activePlugin.setting.moreMenu) {\n                        ((item) => {\n                            menuTemplate.push({\n                                label: item.title,\n                                click: async () => {\n                                    await window.$mapi.manager.firePluginMoreMenuClick(\n                                        item.name,\n                                    );\n                                },\n                            });\n                        })(item);\n                    }\n                }\n            }\n        }\n        if (!menuTemplate.length) {\n            return;\n        }\n        Menu.buildFromTemplate(menuTemplate).popup();\n    };\n\n    const doDetachPlugin = async () => {\n        await manager.detachPlugin();\n    };\n\n    const clipboardFilesInfo = computed<{\n        name: string;\n        extName: string;\n    }>(() => {\n        const result = {\n            name: t(\"main.multipleFiles\"),\n            extName: \"ext.unknown\",\n        };\n        if (manager.currentFiles.length <= 0) {\n            return result;\n        }\n        // 只有一个文件的情况\n        if (manager.currentFiles.length === 1) {\n            const file = manager.currentFiles[0];\n            result.name = file.name;\n            result.extName = file.name;\n            if (file.isDirectory) {\n                result.extName = \"ext.folder\";\n            }\n            if (result.name.endsWith(\".fad\")) {\n                result.name = result.name.substring(0, result.name.length - 4);\n            }\n            return result;\n        }\n        // 如果全部是目录\n        const directoryCount = manager.currentFiles.filter(\n            (f) => f.isDirectory,\n        ).length;\n        if (directoryCount === manager.currentFiles.length) {\n            result.name = t(\"main.multipleFolders\");\n            result.extName = \"ext.folder\";\n            return result;\n        }\n        // 如果全部是文件\n        const fileCount = manager.currentFiles.filter((f) => f.isFile).length;\n        if (fileCount === manager.currentFiles.length) {\n            // 如果全部是图片\n            const imageCount = manager.currentFiles.filter((f) => {\n                const ext = f.name.split(\".\").pop()?.toLowerCase();\n                return [\"jpg\", \"jpeg\", \"png\", \"gif\", \"bmp\", \"webp\"].includes(\n                    ext || \"\",\n                );\n            }).length;\n            if (imageCount === manager.currentFiles.length) {\n                result.name = t(\"main.multipleImages\");\n                result.extName = \"ext.png\";\n                return result;\n            }\n        }\n        return result;\n    });\n\n    const onSearchDoubleClick = () => {\n        if (manager.activePlugin) {\n            doDetachPlugin().then();\n        }\n    };\n\n    return {\n        onSearchDoubleClick,\n        doShowMenu,\n        clipboardFilesInfo,\n    };\n};\n"
  },
  {
    "path": "src/pages/Main/Lib/viewOperate.ts",
    "content": "import { ActionTypeEnum, PluginType } from \"../../../types/Manager\";\nimport { computed, watch } from \"vue\";\nimport { useManagerStore } from \"../../../store/modules/manager\";\nimport { useSettingStore } from \"../../../store/modules/setting\";\n\nconst executePluginHooks = async (web: any, hook: string, data?: any) => {\n    const evalJs = `\n    if(window.focusany && window.focusany.hooks && typeof window.focusany.hooks.on${hook} === 'function' ) {\n        try {\n            window.focusany.hooks.on${hook}(${JSON.stringify(data)});\n        } catch(e) {\n            console.log('executePluginHooks.on${hook}.error', e);\n        }\n    }`;\n    return web.executeJavaScript(evalJs);\n};\n\nconst manager = useManagerStore();\nconst setting = useSettingStore();\n\nexport const useViewOperate = (type: \"fastPanel\" | \"main\") => {\n    const webUserAgent = window.$mapi.app.getUserAgent();\n\n    const viewActions = computed(() => {\n        if (type === \"main\") {\n            return manager.viewActions.map((a) => {\n                a[\"_web\"] = null;\n                a[\"_webInit\"] = false;\n                a[\"_webReady\"] = false;\n                a[\"_height\"] = a.runtime?.view?.heightView || 100;\n                return a;\n            });\n        }\n        return manager.fastPanelViewActions.map((a) => {\n            a[\"_web\"] = null;\n            a[\"_webInit\"] = false;\n            a[\"_webReady\"] = false;\n            a[\"_height\"] = a.runtime?.view?.heightView || 100;\n            return a;\n        });\n    });\n\n    const queryWeb = () => {\n        // console.log('queryWeb.entry', viewActions.value.map(a => a['_web']))\n        for (const a of viewActions.value) {\n            if (a.type !== ActionTypeEnum.VIEW) {\n                continue;\n            }\n            if (!a[\"_web\"] || a[\"_webInit\"]) {\n                continue;\n            }\n            a[\"_webInit\"] = true;\n            // console.log('queryWeb', a['_web'])\n            const readyData = {};\n            readyData[\"actionName\"] = a.name;\n            readyData[\"actionMatch\"] = a.runtime?.match;\n            readyData[\"actionMatchFiles\"] = a.runtime?.matchFiles;\n            readyData[\"requestId\"] = a.runtime?.requestId as any;\n            readyData[\"reenter\"] = false;\n            readyData[\"isView\"] = true;\n            ((aa) => {\n                aa[\"_web\"].addEventListener(\"did-finish-load\", async () => {\n                    aa[\"_webReady\"] = true;\n                    aa[\"_web\"].insertCSS(`body{overflow: hidden;}`);\n                    if (setting.shouldDarkMode()) {\n                        aa[\"_web\"].executeJavaScript(`\n                        document.body.setAttribute('data-theme', 'dark');\n                        document.documentElement.setAttribute('data-theme', 'dark');\n                    `);\n                        if (aa.pluginType === PluginType.SYSTEM) {\n                            aa[\"_web\"].executeJavaScript(\n                                `document.body.setAttribute('arco-theme', 'dark');`,\n                            );\n                        }\n                    }\n                });\n                aa[\"_web\"].addEventListener(\"dom-ready\", async () => {\n                    await executePluginHooks(\n                        a[\"_web\"],\n                        \"PluginReady\",\n                        readyData,\n                    );\n                    if (aa.runtime?.view?.showViewDevTools) {\n                        aa[\"_web\"].openDevTools({\n                            mode: \"detach\",\n                            activate: false,\n                        });\n                    }\n                });\n                aa[\"_web\"].addEventListener(\"ipc-message\", (event) => {\n                    if (\"FocusAny.View\" === event.channel) {\n                        const { id, type, data } = event.args[0];\n                        switch (type) {\n                            case \"view.setHeight\":\n                                aa[\"_height\"] = data.height;\n                                break;\n                            case \"view.getHeight\":\n                                // console.log('view.getHeight', aa['_height'])\n                                aa[\"_web\"].send(\n                                    `FocusAny.View.${id}`,\n                                    aa[\"_height\"],\n                                );\n                                break;\n                        }\n                    }\n                });\n            })(a);\n        }\n    };\n\n    watch(\n        () => viewActions.value,\n        () => {\n            queryWeb();\n        },\n        {\n            deep: true,\n        },\n    );\n\n    return {\n        webUserAgent,\n        viewActions,\n    };\n};\n"
  },
  {
    "path": "src/pages/Main/MainResult.vue",
    "content": "<template>\n    <div class=\"pb-result\" id=\"MainResult_Container\">\n        <div ref=\"resultContainer\">\n            <ResultLoading\n                v-if=\"manager.activePlugin && manager.activePluginLoading\"\n            />\n            <div\n                class=\"action-container\"\n                :class=\"{\n                    'has-actions': hasActions,\n                    'has-view-actions': hasViewActions,\n                }\"\n                v-else-if=\"!manager.activePlugin\"\n            >\n                <div class=\"group-main\">\n                    <div v-if=\"showDetachWindowActions.length\" class=\"group\">\n                        <div class=\"group-title\">\n                            <div class=\"title\">\n                                <img\n                                    draggable=\"false\"\n                                    class=\"dark:invert\"\n                                    :src=\"SystemIcons.searchWindow\"\n                                />\n                                {{ $t(\"main.window\") }}\n                            </div>\n                            <div class=\"more\">&nbsp;</div>\n                        </div>\n                        <div class=\"group-items\">\n                            <div\n                                class=\"item\"\n                                v-for=\"(a, aIndex) in showDetachWindowActions\"\n                                :class=\"{\n                                    active:\n                                        activeActionGroup === 'window' &&\n                                        actionActionIndex === aIndex,\n                                }\"\n                            >\n                                <ResultWindowItem\n                                    :action=\"a\"\n                                    @open=\"openActionWindow('open', a)\"\n                                />\n                            </div>\n                        </div>\n                    </div>\n                    <div v-if=\"showSearchActions.length\" class=\"group\">\n                        <div\n                            class=\"group-title\"\n                            @click=\"doSearchActionExtend\"\n                            :class=\"!searchActionIsExtend ? 'has-more' : ''\"\n                        >\n                            <div class=\"title\">\n                                <img\n                                    draggable=\"false\"\n                                    class=\"dark:invert\"\n                                    :src=\"SystemIcons.searchKeyword\"\n                                />\n                                {{ $t(\"main.searchResults\") }}\n                            </div>\n                            <div class=\"more\" v-if=\"!searchActionIsExtend\">\n                                {{ $t(\"main.expandAll\") }}({{\n                                    manager.searchActions.length\n                                }})\n                            </div>\n                        </div>\n                        <div class=\"group-items\">\n                            <div\n                                class=\"item\"\n                                v-for=\"(a, aIndex) in showSearchActions\"\n                                :class=\"{\n                                    active:\n                                        activeActionGroup === 'search' &&\n                                        actionActionIndex === aIndex,\n                                }\"\n                            >\n                                <ResultItem\n                                    :action=\"a\"\n                                    @open=\"doOpenAction(a)\"\n                                    :show-pin=\"!a.runtime?.isPined\"\n                                    @pin=\"doPinToggle(a)\"\n                                />\n                            </div>\n                        </div>\n                    </div>\n                    <div v-if=\"showMatchActions.length\" class=\"group\">\n                        <div\n                            class=\"group-title\"\n                            @click=\"doMatchActionExtend\"\n                            :class=\"!matchActionIsExtend ? 'has-more' : ''\"\n                        >\n                            <div class=\"title\">\n                                <img\n                                    class=\"dark:invert\"\n                                    :src=\"SystemIcons.searchMatch\"\n                                />\n                                {{ $t(\"main.matchResults\") }}\n                            </div>\n                            <div class=\"more\" v-if=\"!matchActionIsExtend\">\n                                {{ $t(\"main.expandAll\") }}({{\n                                    manager.matchActions.length\n                                }})\n                            </div>\n                        </div>\n                        <div class=\"group-items\">\n                            <div\n                                class=\"item\"\n                                v-for=\"(a, aIndex) in showMatchActions\"\n                                :class=\"{\n                                    active:\n                                        activeActionGroup === 'match' &&\n                                        actionActionIndex === aIndex,\n                                }\"\n                            >\n                                <ResultItem\n                                    :action=\"a\"\n                                    @open=\"doOpenAction(a)\"\n                                    :show-pin=\"!a.runtime?.isPined\"\n                                    @pin=\"doPinToggle(a)\"\n                                />\n                            </div>\n                        </div>\n                    </div>\n                    <div v-if=\"showHistoryActions.length\" class=\"group\">\n                        <div\n                            class=\"group-title\"\n                            :class=\"!historyActionIsExtend ? 'has-more' : ''\"\n                        >\n                            <div class=\"title\">\n                                <icon-history />\n                                {{ $t(\"main.recentlyUsed\") }}\n                            </div>\n                            <div class=\"more\">\n                                <a\n                                    href=\"javascript:;\"\n                                    class=\"auto-hide\"\n                                    @click=\"doHistoryClear\"\n                                >\n                                    <icon-delete />\n                                </a>\n                                <a\n                                    href=\"javascript:;\"\n                                    v-if=\"!historyActionIsExtend\"\n                                    @click=\"doHistoryActionExtend\"\n                                >\n                                    {{ $t(\"main.expandAll\") }}({{\n                                        manager.historyActions.length\n                                    }})\n                                </a>\n                            </div>\n                        </div>\n                        <div class=\"group-items\">\n                            <div\n                                class=\"item\"\n                                v-for=\"(a, aIndex) in showHistoryActions\"\n                                :class=\"{\n                                    active:\n                                        activeActionGroup === 'history' &&\n                                        actionActionIndex === aIndex,\n                                }\"\n                            >\n                                <ResultItem\n                                    :action=\"a\"\n                                    @open=\"doOpenAction(a)\"\n                                    :show-pin=\"!a.runtime?.isPined\"\n                                    @pin=\"doPinToggle(a)\"\n                                    show-delete\n                                    @delete=\"doHistoryDelete(a)\"\n                                />\n                            </div>\n                        </div>\n                    </div>\n                    <div v-if=\"showPinActions.length\" class=\"group\">\n                        <div\n                            class=\"group-title\"\n                            :class=\"!pinActionIsExtend ? 'has-more' : ''\"\n                        >\n                            <div class=\"title\">\n                                <IconPin />\n                                {{ $t(\"main.pinned\") }}\n                            </div>\n                            <div class=\"more\">\n                                <a\n                                    href=\"javascript:;\"\n                                    v-if=\"!pinActionIsExtend\"\n                                    @click=\"doPinActionExtend\"\n                                >\n                                    {{ $t(\"main.expandAll\") }}({{\n                                        manager.pinActions.length\n                                    }})\n                                </a>\n                            </div>\n                        </div>\n                        <div class=\"group-items\">\n                            <div\n                                class=\"item\"\n                                v-for=\"(a, aIndex) in showPinActions\"\n                                :class=\"{\n                                    active:\n                                        activeActionGroup === 'pin' &&\n                                        actionActionIndex === aIndex,\n                                }\"\n                            >\n                                <ResultItem\n                                    :action=\"a\"\n                                    @open=\"doOpenAction(a)\"\n                                    show-pin\n                                    @pin=\"doPinToggle(a)\"\n                                />\n                            </div>\n                        </div>\n                    </div>\n                    <div\n                        v-if=\"\n                            !manager.activePlugin &&\n                            !manager.searchLoading &&\n                            !hasActions &&\n                            manager.searchValue\n                        \"\n                        class=\"group\"\n                    >\n                        <div class=\"group-title\">\n                            <div class=\"title\">\n                                <icon-search />\n                                {{ $t(\"main.searchResults\") }}\n                            </div>\n                        </div>\n                        <div class=\"text-center\" style=\"height: 250px\">\n                            <div class=\"py-4\">\n                                <m-empty />\n                            </div>\n                        </div>\n                    </div>\n                    <div\n                        v-if=\"!manager.activePlugin && hasActions\"\n                        style=\"height: 10px\"\n                    ></div>\n                </div>\n                <div class=\"group-right\" v-if=\"viewActions.length > 0\">\n                    <div class=\"view\" v-if=\"viewActions.length\">\n                        <div v-for=\"r in viewActions\" class=\"view-item\">\n                            <div class=\"view-item-head\">\n                                <div class=\"icon\">\n                                    <img\n                                        :src=\"r.icon\"\n                                        :class=\"\n                                            r.pluginType === PluginType.SYSTEM\n                                                ? 'dark:invert'\n                                                : 'plugin-logo-filter'\n                                        \"\n                                    />\n                                </div>\n                                <div class=\"text\">\n                                    {{ r.title }}\n                                </div>\n                                <div v-if=\"0\" class=\"action\">\n                                    <a href=\"javascript:;\">\n                                        {{ $t(\"common.close\") }}\n                                    </a>\n                                    <a href=\"javascript:;\">\n                                        <icon-more-vertical />\n                                    </a>\n                                </div>\n                            </div>\n                            <div class=\"view-item-body\">\n                                <webview\n                                    class=\"web\"\n                                    :ref=\"(el) => (r['_web'] = el)\"\n                                    :style=\"{ height: r['_height'] + 'px' }\"\n                                    :id=\"r.fullName\"\n                                    :preload=\"r.runtime?.view?.preloadBase\"\n                                    :src=\"r.runtime?.view?.mainView\"\n                                    :nodeintegration=\"\n                                        r.runtime?.view?.nodeIntegration\n                                    \"\n                                    :useragent=\"`${webUserAgent} PluginAction/${r.fullName}`\"\n                                    webpreferences=\"contextIsolation=false,sandbox=false\"\n                                    disablewebsecurity\n                                ></webview>\n                                <div\n                                    class=\"view-item-loading\"\n                                    v-if=\"!r['_webReady']\"\n                                >\n                                    <icon-loading />\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div\n                v-else-if=\"\n                    manager.activePlugin && manager.activePluginType === 'code'\n                \"\n            >\n                <ResultActionCodeError\n                    v-if=\"manager.actionCodeError\"\n                    :error=\"manager.actionCodeError\"\n                />\n                <ResultActionCodeLoading\n                    v-else-if=\"manager.actionCodeLoading\"\n                />\n                <ResultActionCodeItemList\n                    v-else-if=\"'list' === manager.actionCodeType\"\n                    :do-open-action-code=\"doOpenActionCode\"\n                    :is-osx=\"isOsx\"\n                />\n            </div>\n        </div>\n    </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { onBeforeMount, ref } from \"vue\";\nimport IconPin from \"~icons/mdi/pin\";\nimport { SystemIcons } from \"../../../electron/mapi/manager/system/asset/icon\";\nimport MEmpty from \"../../components/common/MEmpty.vue\";\nimport { useManagerStore } from \"../../store/modules/manager\";\nimport { PluginType } from \"../../types/Manager\";\nimport ResultActionCodeError from \"./Components/ResultActionCodeError.vue\";\nimport ResultActionCodeItemList from \"./Components/ResultActionCodeItemList.vue\";\nimport ResultActionCodeLoading from \"./Components/ResultActionCodeLoading.vue\";\nimport ResultItem from \"./Components/ResultItem.vue\";\nimport ResultLoading from \"./Components/ResultLoading.vue\";\nimport ResultWindowItem from \"./Components/ResultWindowItem.vue\";\nimport { useResultOperate } from \"./Lib/resultOperate\";\nimport { fireResultResize, useResultResize } from \"./Lib/resultResize\";\nimport { useViewOperate } from \"./Lib/viewOperate\";\n\nconst manager = useManagerStore();\n\nconst {\n    hasActions,\n    hasViewActions,\n    searchActionIsExtend,\n    matchActionIsExtend,\n    historyActionIsExtend,\n    pinActionIsExtend,\n    doSearchActionExtend,\n    doMatchActionExtend,\n    doHistoryActionExtend,\n    doPinActionExtend,\n    showDetachWindowActions,\n    showSearchActions,\n    showMatchActions,\n    showHistoryActions,\n    showPinActions,\n    activeActionGroup,\n    actionActionIndex,\n    onInputKey,\n    onClose,\n    doOpenAction,\n    doOpenActionCode,\n    openActionWindow,\n    doHistoryClear,\n    doHistoryDelete,\n    doPinToggle,\n} = useResultOperate();\n\nconst isOsx = ref(false);\n\nonBeforeMount(async () => {\n    isOsx.value = window.$mapi.app.isPlatform(\"osx\");\n});\n\nconst { webUserAgent, viewActions } = useViewOperate(\"main\");\n\nconst emit = defineEmits([]);\n\nconst resultContainer = ref<HTMLElement | null>(null);\nuseResultResize(resultContainer);\n\nconst onPluginExit = () => {\n    fireResultResize(resultContainer);\n};\n\nconst onPluginDetached = () => {\n    fireResultResize(resultContainer);\n};\n\nconst onInputHotKey = (e: KeyboardEvent) => {\n    if ([\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"].includes(e.key)) {\n        let pass = false;\n        if (isOsx.value) {\n            if (e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {\n                pass = true;\n            }\n        } else {\n            if (e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) {\n                pass = true;\n            }\n        }\n        if (pass) {\n            const index = parseInt(e.key);\n            const item = manager.actionCodeItems.find((o) => {\n                return o.shortcutIndex === index;\n            });\n            if (item) {\n                doOpenActionCode(item.id);\n            }\n        }\n    }\n};\n\ndefineExpose({\n    onInputKey,\n    onInputHotKey,\n    onClose,\n    onPluginExit,\n    onPluginDetached,\n});\n</script>\n\n<style lang=\"less\" scoped>\n.pb-result {\n    overflow-y: auto;\n    overflow-x: hidden;\n    max-height: calc(100vh - 60px);\n    user-select: none;\n\n    &::-webkit-scrollbar {\n        width: 6px;\n        height: 6px;\n    }\n\n    &::-webkit-scrollbar-track {\n        background: #f1f1f1;\n        border-radius: 3px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n        background: #aaaaaa;\n        border-radius: 3px;\n    }\n\n    &::-webkit-scrollbar-thumb:hover {\n        background: #bbbbbb;\n    }\n\n    .action-container {\n        display: flex;\n\n        .group-main {\n            flex-grow: 1;\n        }\n\n        .group-right {\n            width: 260px;\n            border-left: 1px solid var(--color-border);\n            border-top: 1px solid var(--color-border);\n            border-radius: 0.5rem 0 0 0;\n            flex-shrink: 0;\n            box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);\n            margin-top: 5px;\n\n            .view-item-head {\n                display: flex;\n                align-items: center;\n                padding: 2px 5px;\n                font-size: 12px;\n                height: 26px;\n\n                &:hover {\n                    .action {\n                        display: block;\n                    }\n                }\n\n                .icon {\n                    width: 16px;\n                    height: 16px;\n                    margin-right: 5px;\n\n                    img {\n                        width: 16px;\n                        height: 16px;\n                        object-fit: contain;\n                    }\n                }\n\n                .text {\n                    flex: 1;\n                    user-select: none;\n                }\n\n                .action {\n                    a {\n                        display: inline-block;\n                        border-radius: 5px;\n                        height: 20px;\n                        min-width: 20px;\n                        text-align: center;\n                        font-size: 10px;\n                        line-height: 20px;\n                        padding: 0 5px;\n                        margin-left: 2px;\n\n                        &:hover {\n                            background: #f0f0f0;\n                        }\n                    }\n                }\n            }\n\n            .view-item-body {\n                position: relative;\n\n                .web {\n                    transition: height 0.3s;\n                }\n\n                .view-item-loading {\n                    position: absolute;\n                    inset: 0;\n                    display: flex;\n                    justify-content: center;\n                    align-items: center;\n                    background: rgba(255, 255, 255, 0.8);\n                    z-index: 1;\n                }\n            }\n        }\n    }\n\n    .group {\n        padding-top: 10px;\n\n        .group-title {\n            height: 30px;\n            display: flex;\n            align-items: center;\n            padding: 0 10px;\n            cursor: pointer;\n            margin-bottom: 3px;\n\n            &:hover {\n                .more {\n                    a.auto-hide {\n                        display: inline-block;\n                    }\n                }\n            }\n\n            .title {\n                flex-grow: 1;\n                font-weight: bold;\n                font-size: 16px;\n                display: flex;\n                align-items: center;\n\n                img,\n                svg {\n                    width: 20px;\n                    height: 20px;\n                    object-fit: contain;\n                    margin-right: 5px;\n                    display: block;\n                    line-height: 20px;\n                    text-align: center;\n                }\n            }\n\n            .more {\n                color: #999;\n                cursor: pointer;\n\n                a {\n                    display: inline-block;\n                    padding: 0.1rem 0.3rem;\n                    border-radius: 0.3rem;\n\n                    &.auto-hide {\n                        display: none;\n                    }\n\n                    &:hover {\n                        background: #eee;\n                    }\n                }\n            }\n        }\n\n        .group-items {\n            display: flex;\n            flex-wrap: wrap;\n            padding: 0 10px;\n\n            .item {\n                width: 96px;\n                //margin-right: 2px;\n                height: 100px;\n                flex-shrink: 0;\n                text-align: center;\n\n                &.active {\n                    :deep(.item-box) {\n                        background-color: #eeeeee;\n                    }\n\n                    :deep(.item-window-box) {\n                        border-color: #eaa109;\n                        background-color: #f8dfab;\n                    }\n                }\n            }\n        }\n    }\n}\n\n[data-theme=\"dark\"] {\n    .pb-result {\n        &::-webkit-scrollbar-track {\n            background: #333;\n        }\n\n        &::-webkit-scrollbar-thumb {\n            background: #555;\n        }\n\n        &::-webkit-scrollbar-thumb:hover {\n            background: #777;\n        }\n\n        .group {\n            .group-title {\n                &.has-more {\n                    &:hover {\n                        background: #333;\n                    }\n                }\n            }\n\n            .group-items {\n                .item {\n                    &.active {\n                        :deep(.item-box) {\n                            background-color: #333;\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/Main/MainSearch.vue",
    "content": "<template>\n    <div class=\"pb-search\" @mousedown=\"onDragWindowMouseDown\">\n        <div class=\"content-left\">\n            <div v-if=\"!manager.activePlugin\" @click=\"doLogoClick\" class=\"logo\">\n                <img src=\"./../../assets/image/search-icon.svg\" />\n            </div>\n            <div\n                v-if=\"\n                    !manager.activePlugin &&\n                    (manager.currentFiles.length ||\n                        manager.currentImage ||\n                        manager.currentText)\n                \"\n                class=\"attachment\"\n            >\n                <div v-if=\"manager.currentFiles.length > 0\" class=\"file\">\n                    <div class=\"type\">\n                        <FileExt :name=\"clipboardFilesInfo.extName\" />\n                    </div>\n                    <div class=\"title\">{{ clipboardFilesInfo.name }}</div>\n                    <div class=\"count\" v-if=\"manager.currentFiles.length > 1\">\n                        x{{ manager.currentFiles.length }}\n                    </div>\n                </div>\n                <div v-else-if=\"manager.currentImage\" class=\"image\">\n                    <img :src=\"manager.currentImage\" />\n                </div>\n                <div v-else-if=\"manager.currentText\" class=\"text\">\n                    <IconFormatText />\n                    <div class=\"content\">\n                        {{ manager.currentText }}\n                    </div>\n                </div>\n                <div\n                    class=\"close text-gray-500 bg-gray-200 hover:bg-gray-500 hover:text-white\"\n                    @click=\"emit('onClose')\"\n                >\n                    <icon-close />\n                </div>\n            </div>\n            <div\n                v-if=\"manager.activePlugin\"\n                class=\"plugin bg-gray-200 dark:bg-gray-600\"\n            >\n                <div class=\"icon\">\n                    <img\n                        :src=\"manager.activePlugin.logo\"\n                        :class=\"\n                            manager.activePlugin.type === PluginType.SYSTEM\n                                ? 'dark:invert'\n                                : 'plugin-logo-filter'\n                        \"\n                    />\n                </div>\n                <div class=\"title\">\n                    {{ manager.activePlugin.title }}\n                </div>\n                <div\n                    class=\"close text-gray-500 hover:bg-gray-500 hover:text-white\"\n                    @click=\"emit('onClose')\"\n                >\n                    <icon-close />\n                </div>\n            </div>\n        </div>\n        <div\n            v-if=\"!manager.activePlugin || manager.searchSubIsVisible\"\n            class=\"main-search\"\n        >\n            <a-input\n                id=\"search\"\n                ref=\"mainInput\"\n                size=\"large\"\n                @input=\"(e) => onSearchValueChange(e)\"\n                @blur=\"onBlur\"\n                @dblclick=\"onSearchDoubleClick\"\n                @compositionstart=\"\n                    (e) => {\n                        manager.searchIsCompositing = true;\n                    }\n                \"\n                @compositionend=\"\n                    (e) => {\n                        manager.searchIsCompositing = false;\n                    }\n                \"\n                :model-value=\"manager.searchValue\"\n            >\n            </a-input>\n            <div\n                class=\"placeholder\"\n                v-if=\"\n                    manager.searchValue === '' &&\n                    searchValueCompositingValue === '' &&\n                    !manager.searchIsCompositing\n                \"\n            >\n                {{\n                    manager.activePlugin\n                        ? manager.searchSubPlaceholder\n                        : manager.searchPlaceholder\n                }}\n            </div>\n        </div>\n        <div v-else @dblclick=\"onSearchDoubleClick\" class=\"main-search\"></div>\n        <div class=\"content-right\" @click=\"doShowMenu\">\n            <div class=\"more\" v-if=\"manager.activePlugin\">\n                <icon-more-vertical style=\"font-size: 20px\" />\n            </div>\n        </div>\n        <div\n            v-if=\"!!manager.notice && manager.notice.text\"\n            class=\"flex items-center bg-yellow-100 shadow leading-8 px-2 rounded-lg\"\n        >\n            <div class=\"mr-1\">\n                <icon-info-circle v-if=\"manager.notice.type === 'info'\" />\n                <icon-check-circle\n                    v-else-if=\"manager.notice.type === 'success'\"\n                />\n                <icon-close-circle\n                    v-else-if=\"manager.notice.type === 'error'\"\n                />\n            </div>\n            <div>\n                {{ manager.notice.text }}\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, watch } from \"vue\";\nimport IconFormatText from \"~icons/mdi/format-text\";\nimport { useDragWindow } from \"../../app/dragWindow\";\nimport FileExt from \"../../components/common/FileExt.vue\";\nimport { useManagerStore } from \"../../store/modules/manager\";\nimport { PluginType } from \"../../types/Manager\";\nimport { EntryListener } from \"./Lib/entryListener\";\nimport { useSearchOperate } from \"./Lib/searchOperate\";\n\nconst mainInput = ref<any>(null);\n\nconst emit = defineEmits([\"onClose\"]);\n\nconst manager = useManagerStore();\n\nconst { onDragWindowMouseDown } = useDragWindow({\n    name: \"main\",\n    ignore: (e) => {\n        return !!(e.target && (e.target as any).tagName === \"INPUT\");\n    },\n});\n\nconst { onSearchDoubleClick, doShowMenu, clipboardFilesInfo } =\n    useSearchOperate(emit);\n\nlet input = {\n    ele: null as any,\n    context: null as any,\n};\nlet searchValueCompositingValue = ref(\"\");\n\nconst updateWidth = () => {\n    if (!input.ele) {\n        input.ele = document.querySelector('.pb-search input[type=\"text\"]');\n        const canvas = document.createElement(\"canvas\");\n        input.context = canvas.getContext(\"2d\") as any;\n        input.context.font = window.getComputedStyle(input.ele).font;\n        if (input.ele) {\n            input.ele.addEventListener(\"input\", (event) => {\n                updateWidth();\n            });\n        }\n    }\n    searchValueCompositingValue.value = input.ele.value || manager.searchValue;\n    const width = input.context.measureText(\n        searchValueCompositingValue.value,\n    ).width;\n    // console.log('width', {value: searchValueCompositingValue.value, value2: manager.searchValue, width})\n    input.ele.style.width = width + 10 + \"px\";\n};\n\nwatch(\n    () => manager.searchValue,\n    (value) => {\n        nextTick(() => {\n            updateWidth();\n        });\n    },\n);\n\nonMounted(() => {\n    updateWidth();\n});\n\nconst onSearchValueChange = (value: string) => {\n    manager.search(value);\n};\n\nconst onShow = () => {\n    mainInput.value?.focus();\n    EntryListener.prepareSearch({}).then();\n};\n\nconst focus = (search: boolean) => {\n    mainInput.value?.focus();\n    if (search) {\n        EntryListener.prepareSearch({}).then();\n    }\n};\n\nconst doLogoClick = () => {\n    window.focusany.redirect([\"system\", \"page-setting\"]);\n};\n\nconst onBlur = () => {\n    setTimeout(() => {\n        mainInput.value?.focus();\n    }, 0);\n};\n\ndefineExpose({\n    onShow,\n    focus,\n});\n</script>\n\n<style scoped lang=\"less\">\n.pb-search {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    height: 60px;\n    padding: 10px;\n    background-color: var(--color-background);\n    user-select: none;\n    z-index: 1;\n\n    .content-left {\n        display: flex;\n        align-items: center;\n        flex-shrink: 0;\n        border-radius: 30px;\n        transition: all 0.2s;\n\n        .logo {\n            cursor: pointer;\n            width: 40px;\n            height: 40px;\n            border-radius: 50%;\n            transition: box-shadow 0.5s;\n            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);\n\n            &:hover {\n                box-shadow: 0 0 10px rgba(0, 0, 0, 0.8);\n            }\n\n            img {\n                width: 40px;\n                height: 40px;\n                border-radius: 50%;\n                object-fit: contain;\n            }\n        }\n\n        .attachment {\n            height: 36px;\n            line-height: 36px;\n            display: flex;\n            align-items: center;\n            margin-left: 5px;\n            background: var(--color-background);\n            position: relative;\n\n            .close {\n                position: absolute;\n                right: 0;\n                top: -4px;\n            }\n\n            .file {\n                display: flex;\n                align-items: center;\n                margin-right: 5px;\n                background: var(--color-background-content);\n                border-radius: 5px;\n                padding: 0 10px;\n\n                .type {\n                    width: 20px;\n                    height: 20px;\n                    margin-right: 5px;\n                    font-size: 0;\n                }\n\n                .title {\n                    color: #333;\n                    font-weight: bold;\n                }\n\n                .count {\n                    line-height: 20px;\n                    color: #fff;\n                    background: #cf0707;\n                    border-radius: 10px;\n                    padding: 0 5px;\n                    margin-left: 5px;\n                    font-size: 12px;\n                }\n            }\n\n            .image {\n                margin-right: 5px;\n\n                img {\n                    max-height: 30px;\n                    max-width: 60px;\n                    border-radius: 5px;\n                    box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);\n                    background-color: var(--color-background);\n                }\n            }\n\n            .text {\n                height: 30px;\n                line-height: 30px;\n                padding: 0 10px;\n                margin-right: 5px;\n                border-radius: 5px;\n                box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);\n                background: var(--color-background-content);\n                max-width: 10rem;\n                display: flex;\n                align-items: center;\n                flex-wrap: nowrap;\n\n                svg {\n                    flex-shrink: 0;\n                    width: 20px;\n                    height: 20px;\n                    line-height: 20px;\n                    display: block;\n                }\n\n                .content {\n                    overflow: hidden;\n                    text-overflow: ellipsis;\n                    white-space: nowrap;\n                }\n            }\n        }\n\n        .plugin {\n            display: flex;\n            align-items: center;\n            margin-right: 5px;\n            padding: 0 10px;\n            height: 40px;\n            border-radius: 20px;\n\n            .icon {\n                width: 20px;\n                height: 20px;\n                margin-right: 5px;\n\n                img {\n                    width: 20px;\n                    height: 20px;\n                    object-fit: contain;\n                }\n            }\n\n            .title {\n                line-height: 20px;\n                font-weight: bold;\n                margin-right: 5px;\n            }\n\n            .close {\n                width: 20px;\n                height: 20px;\n\n                :deep(.arco-icon) {\n                    width: 16px;\n                    height: 16px;\n                    margin: auto;\n                }\n            }\n        }\n\n        .close {\n            width: 14px;\n            height: 14px;\n            border-radius: 50%;\n            cursor: pointer;\n            padding: 0;\n            font-size: 0;\n            display: flex;\n\n            :deep(.arco-icon) {\n                width: 10px;\n                height: 10px;\n                margin: auto;\n            }\n\n            &:hover {\n                background: rgba(255, 0, 0, 0.8);\n            }\n        }\n    }\n\n    .main-search {\n        height: 40px !important;\n        flex: 1;\n        width: 0;\n        position: relative;\n\n        .placeholder {\n            position: absolute;\n            top: 0;\n            left: 10px;\n            line-height: 40px;\n            font-size: 20px;\n            color: #999;\n            z-index: 0;\n        }\n\n        :deep(.arco-input-wrapper) {\n            position: absolute;\n            z-index: 1;\n            height: 40px !important;\n            box-sizing: border-box;\n            border: none;\n            outline: none;\n            box-shadow: none !important;\n            background-color: transparent;\n            padding-left: 8px;\n            cursor: default;\n        }\n\n        :deep(input) {\n            cursor: text !important;\n        }\n\n        &:hover {\n            background-color: transparent !important;\n        }\n\n        :deep(.arco-input) {\n            font-size: 20px !important;\n        }\n    }\n\n    .content-right {\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageAbout.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport FeedbackTicketButton from \"../components/common/FeedbackTicketButton.vue\";\nimport UpdaterButton from \"../components/common/UpdaterButton.vue\";\nimport { AppConfig } from \"../config\";\nimport { t } from \"../lang\";\nimport { useSettingStore } from \"../store/modules/setting\";\n\nconst setting = useSettingStore();\nconst licenseYear = new Date().getFullYear();\nconst devSettingVisible = ref(false);\n\nconst doOpenLog = async () => {\n    await window.$mapi.app.openPath(window.$mapi.log.root());\n};\nlet clickTimes = 0;\nlet clickLastTime = 0;\nconst doDevSettingTriggerClick = () => {\n    // click more than 5 times in 3 seconds\n    const now = new Date().getTime();\n    if (0 === clickLastTime) {\n        clickLastTime = now;\n    }\n    if (now - clickLastTime < 3000) {\n        clickTimes++;\n        if (clickTimes >= 5) {\n            devSettingVisible.value = true;\n            clickTimes = 0;\n        }\n    } else {\n        clickTimes = 0;\n    }\n};\n</script>\n\n<template>\n    <div class=\"flex overflow-auto\" style=\"height: calc(100vh - 2.5rem)\">\n        <div class=\"p-4 m-auto\">\n            <div class=\"flex pb-6\">\n                <div class=\"m-auto\" @click=\"doDevSettingTriggerClick\">\n                    <div>\n                        <img\n                            class=\"w-14 h-14 mx-auto\"\n                            src=\"./../assets/image/logo.svg\"\n                        />\n                    </div>\n                    <div class=\"text-xl pt-2 font-bold\">\n                        {{ AppConfig.title }}\n                    </div>\n                </div>\n            </div>\n            <div\n                v-if=\"devSettingVisible\"\n                class=\"bg-gray-100 p-3 mb-3 rounded-lg\"\n            >\n                <div class=\"flex mb-4 items-center\">\n                    <icon-code class=\"mr-2\" />\n                    {{ $t(\"开发模式设置\") }}\n                </div>\n                <div class=\"flex mb-4\">\n                    <div class=\"flex-grow\">{{ $t(\"快速面板失焦隐藏\") }}</div>\n                    <div>\n                        <a-radio-group\n                            :model-value=\"\n                                setting.configEnvGet('fastPanelAutoHide', true)\n                                    .value\n                            \"\n                            @change=\"\n                                setting.onConfigEnvChange(\n                                    'fastPanelAutoHide',\n                                    $event,\n                                )\n                            \"\n                        >\n                            <a-radio :value=\"true\">是</a-radio>\n                            <a-radio :value=\"false\">否</a-radio>\n                        </a-radio-group>\n                    </div>\n                </div>\n            </div>\n            <div class=\"flex mb-3 items-start\">\n                <div class=\"w-20\">{{ t(\"版本\") }}</div>\n                <div class=\"flex-grow\">\n                    <div>\n                        v{{ AppConfig.version }} Build\n                        {{ setting.buildInfo.buildId }}\n                    </div>\n                    <div class=\"pt-2\">\n                        <UpdaterButton />\n                    </div>\n                </div>\n            </div>\n            <div class=\"flex mb-3 items-center\">\n                <div class=\"w-20\">{{ t(\"官网\") }}</div>\n                <div class=\"flex-grow\">\n                    <a\n                        :href=\"AppConfig.website\"\n                        target=\"_blank\"\n                        class=\"text-link\"\n                    >\n                        {{ AppConfig.website }}\n                    </a>\n                </div>\n                <div>\n                    <div class=\"inline-block ml-3\">\n                        <FeedbackTicketButton />\n                    </div>\n                    <a-button class=\"ml-3\" size=\"mini\" @click=\"doOpenLog\">\n                        <template #icon>\n                            <icon-file />\n                        </template>\n                        {{ t(\"日志\") }}\n                    </a-button>\n                </div>\n            </div>\n            <div class=\"text-gray-400 text-center select-none\">\n                &copy; {{ licenseYear }} {{ AppConfig.name }}\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/pages/PageDetachWindow.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeMount, ref } from \"vue\";\nimport { PluginRecord, PluginState, PluginType } from \"../types/Manager\";\nimport { useDetachWindowOperate } from \"./DetachWindow/operate\";\nimport debounce from \"lodash/debounce\";\nimport { useSettingStore } from \"../store/modules/setting\";\n\nconst setting = useSettingStore();\n\nconst settingDummy = setting;\n\n// const id = ref('')\nconst isOsx = ref(false);\nconst isFullscreen = ref(false);\nconst plugin = ref<PluginRecord | null>(null);\nconst alwaysOnTop = ref(false);\nconst operates = ref<{ name: string; title: string }[]>([]);\nconst searchInput = ref<any>(null);\nconst searchPlaceholder = ref(\"\");\nconst searchValue = ref(\"\");\nconst searchVisible = ref(false);\nconst windowTitle = ref(\"\");\n\nconst subInputChangeDebounce = debounce((keywords) => {\n    window.$mapi.manager.subInputChange(keywords);\n}, 300);\n\nonBeforeMount(async () => {\n    isOsx.value = window.$mapi.app.isPlatform(\"osx\");\n});\n\nconst doToggleAlwaysOnTop = async () => {\n    alwaysOnTop.value =\n        await window.$mapi.manager.toggleDetachPluginAlwaysOnTop(\n            !alwaysOnTop.value,\n        );\n};\n\nconst doOperate = async (name: string) => {\n    await window.$mapi.manager.fireDetachOperateClick(name);\n};\n\nconst onSubInputChange = (value: string) => {\n    searchValue.value = value;\n    subInputChangeDebounce(value);\n};\n\n// const getId = () => id.value\n\nconst { doShowZoomMenu, doShowMoreMenu, doClose } = useDetachWindowOperate({\n    plugin,\n});\n\nwindow.__page.onPluginInit(\n    (data: {\n        plugin: PluginRecord;\n        state: PluginState;\n        param: {\n            alwaysOnTop: boolean;\n        };\n    }) => {\n        // console.log('onPluginInit', data)\n        plugin.value = data.plugin;\n        // id.value = data.param.id\n        alwaysOnTop.value = data.param.alwaysOnTop;\n        searchValue.value = data.state.value;\n        searchPlaceholder.value = data.state.placeholder;\n        searchVisible.value = data.state.isVisible;\n        windowTitle.value = data.plugin.title;\n    },\n);\nwindow.__page.onSetSubInput(\n    (param: { placeholder: string; isFocus: boolean; isVisible: boolean }) => {\n        searchPlaceholder.value = param.placeholder;\n        searchVisible.value = param.isVisible;\n        if (param.isFocus && searchInput.value) {\n            searchInput.value.focus();\n        }\n    },\n);\nwindow.__page.onRemoveSubInput(() => {\n    searchPlaceholder.value = \"\";\n});\nwindow.__page.onSetSubInputValue((value: string) => {\n    searchValue.value = value;\n});\nwindow.__page.onDetachSet(\n    (param: {\n        title?: string;\n        alwaysOnTop?: boolean;\n        operates?: {\n            name: string;\n            title: string;\n        }[];\n    }) => {\n        if (\"title\" in param) {\n            windowTitle.value = param.title as string;\n        }\n        if (\"alwaysOnTop\" in param) {\n            alwaysOnTop.value = param.alwaysOnTop as boolean;\n        }\n        if (\"operates\" in param) {\n            operates.value = param.operates as {\n                name: string;\n                title: string;\n            }[];\n        }\n    },\n);\nwindow.__page.onMaximize(() => {\n    console.log(\"onMaximize\");\n});\nwindow.__page.onUnmaximize(() => {\n    console.log(\"onUnmaximize\");\n});\nwindow.__page.onEnterFullScreen(() => {\n    isFullscreen.value = true;\n    console.log(\"onEnterFullScreen\");\n});\nwindow.__page.onLeaveFullScreen(() => {\n    isFullscreen.value = false;\n    console.log(\"onLeaveFullScreen\");\n});\n</script>\n\n<template>\n    <div\n        class=\"pb-page-detach-window\"\n        v-if=\"!!plugin\"\n        :class=\"{ osx: isOsx, fullscreen: isFullscreen }\"\n    >\n        <div class=\"head\">\n            <div class=\"left\">\n                <div class=\"icon\">\n                    <img\n                        :src=\"plugin.logo\"\n                        :class=\"\n                            plugin.type === PluginType.SYSTEM\n                                ? 'dark:invert'\n                                : 'plugin-logo-filter'\n                        \"\n                    />\n                </div>\n                <div class=\"title\">\n                    {{ windowTitle }}\n                </div>\n            </div>\n            <div v-if=\"operates.length\" class=\"operate\">\n                <a-button\n                    v-for=\"o in operates\"\n                    @click=\"doOperate(o.name)\"\n                    class=\"operate-item\"\n                    shape=\"round\"\n                    size=\"small\"\n                >\n                    {{ o.title }}\n                </a-button>\n            </div>\n            <div class=\"search\" v-if=\"searchVisible\">\n                <a-input\n                    ref=\"searchInput\"\n                    size=\"small\"\n                    @input=\"(e) => onSubInputChange(e)\"\n                    :model-value=\"searchValue\"\n                    :placeholder=\"searchPlaceholder\"\n                >\n                    <template #prefix>\n                        <icon-search />\n                    </template>\n                </a-input>\n            </div>\n            <div class=\"right\">\n                <a\n                    href=\"javascript:;\"\n                    @click=\"doToggleAlwaysOnTop\"\n                    :class=\"{ active: alwaysOnTop }\"\n                >\n                    <icon-pushpin />\n                </a>\n                <span class=\"line\"></span>\n                <a href=\"javascript:;\" @click=\"doShowZoomMenu\">\n                    <icon-zoom-in />\n                </a>\n                <span class=\"line\"></span>\n                <a href=\"javascript:;\" @click=\"doShowMoreMenu\">\n                    <icon-more />\n                </a>\n                <span class=\"line\" v-if=\"!isOsx\"></span>\n                <a href=\"javascript:;\" v-if=\"!isOsx\" @click=\"doClose\">\n                    <icon-close />\n                </a>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n[data-theme=\"dark\"] {\n    .pb-page-detach-window {\n        .head {\n            background: #333333;\n            color: #ffffff;\n\n            .right {\n                border-color: #666666;\n            }\n        }\n    }\n}\n\n.pb-page-detach-window {\n    &.osx {\n        .head {\n            padding-left: 80px;\n        }\n\n        &.fullscreen {\n            .head {\n                padding-left: 10px;\n            }\n        }\n    }\n\n    .head {\n        padding: 0 10px;\n        height: 40px;\n        background: #ffffff;\n        display: flex;\n        align-items: center;\n        -webkit-user-select: none;\n        transition: padding-left 0.3s;\n\n        .left {\n            flex-grow: 1;\n            display: flex;\n            align-items: center;\n            -webkit-app-region: drag;\n\n            .icon {\n                margin-right: 10px;\n                width: 20px;\n                flex-shrink: 0;\n\n                img {\n                    width: 20px;\n                    height: 20px;\n                    border-radius: 50%;\n                }\n            }\n\n            .title {\n            }\n        }\n\n        .operate {\n            padding-right: 10px;\n\n            .operate-item {\n                margin-left: 10px;\n            }\n        }\n\n        .search {\n            //max-width: 15rem;\n            padding-right: 10px;\n\n            :deep(.arco-input-wrapper) {\n                border-radius: 30px;\n                padding: 0 10px;\n                height: 30px;\n            }\n        }\n\n        .right {\n            border: 1px solid var(--color-border);\n            height: 30px;\n            line-height: 30px;\n            border-radius: 15px;\n            display: flex;\n            align-items: center;\n            background-color: var(--color-background);\n\n            a {\n                display: block;\n                width: 30px;\n                text-align: center;\n                color: var(--color-text);\n\n                &:hover,\n                &.active {\n                    color: var(--color-primary);\n                }\n            }\n\n            .line {\n                height: 15px;\n                width: 1px;\n                background-color: var(--color-border);\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageFastPanel.vue",
    "content": "<template>\n    <a-config-provider :locale=\"locale\" :global=\"true\">\n        <div ref=\"main\" id=\"main\">\n            <FastPanelSearch ref=\"mainSearch\" />\n            <FastPanelResult ref=\"mainResult\" />\n        </div>\n    </a-config-provider>\n</template>\n\n<script setup lang=\"ts\">\nimport { useLocale } from \"../app/locale\";\nimport { ref } from \"vue\";\nimport FastPanelSearch from \"./FastPanel/FastPanelSearch.vue\";\nimport FastPanelResult from \"./FastPanel/FastPanelResult.vue\";\nimport MainSearch from \"./Main/MainSearch.vue\";\nimport MainResult from \"./Main/MainResult.vue\";\nimport { useSettingStore } from \"../store/modules/setting\";\nimport { useManagerStore } from \"../store/modules/manager\";\n\nconst setting = useSettingStore();\n// do not remove this line, it is used to trigger the setting store to be initialized\nconst settingDummy = setting;\nconst manager = useManagerStore();\n\nconst { locale } = useLocale();\n\nconst main = ref<HTMLElement | null>(null);\nconst mainSearch = ref<InstanceType<typeof MainSearch> | null>(null);\nconst mainResult = ref<InstanceType<typeof MainResult> | null>(null);\n\nwindow.__page.onShow(() => {\n    manager.showFirstRun = true;\n    mainSearch.value?.onShow();\n});\n</script>\n\n<style lang=\"less\" scoped>\n#main {\n    height: 100vh;\n    overflow-x: hidden;\n    overflow-y: visible;\n\n    &::-webkit-scrollbar {\n        width: 6px;\n        height: 6px;\n    }\n\n    &::-webkit-scrollbar-track {\n        border-radius: 6px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n        background: #666;\n        border-radius: 6px;\n    }\n\n    &::-webkit-scrollbar-thumb:hover {\n        background: #999;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageFeedback.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, onMounted, ref } from \"vue\";\nimport { AppConfig } from \"../config\";\nimport { t } from \"../lang\";\nimport UpdaterButton from \"../components/common/UpdaterButton.vue\";\nimport { useSettingStore } from \"../store/modules/setting\";\nimport FeedbackTicketButton from \"../components/common/FeedbackTicketButton.vue\";\nimport PageWebviewStatus from \"../components/common/PageWebviewStatus.vue\";\n\nconst setting = useSettingStore();\n\nconst status = ref<InstanceType<typeof PageWebviewStatus> | null>(null);\nconst web = ref<any | null>(null);\nconst webPreload = ref(\"\");\nconst webUrl = ref(\"\");\nconst webUserAgent = window.$mapi.app.getUserAgent();\n\nonMounted(async () => {\n    status.value?.setStatus(\"loading\");\n    webPreload.value = await window.$mapi.app.getPreload();\n    webUrl.value = AppConfig.feedbackUrl;\n    nextTick(() => {\n        web.value.addEventListener(\"did-fail-load\", (event: any) => {\n            status.value?.setStatus(\"fail\");\n        });\n        web.value.addEventListener(\"dom-ready\", async (e) => {\n            const appEnv = await window.$mapi.app.appEnv();\n            web.value.executeJavaScript(\n                `window.$mapi.app.setRenderAppEnv(${JSON.stringify(appEnv)})`,\n            );\n            // web.value.openDevTools()\n            window.$mapi.user.refresh();\n            web.value.executeJavaScript(`\ndocument.addEventListener('click', (event) => {\n    const target = event.target;\n    if (target.tagName !== 'A') return;\n    const url = target.href\n    if(url.startsWith('javascript:')) return;\n    const urlPath = new URL(url).pathname;\n    event.preventDefault();\n    window.$mapi.user.openWebUrl(url)\n});\n`);\n            status.value?.setStatus(\"success\");\n        });\n    });\n});\n</script>\n\n<template>\n    <div style=\"height: calc(100vh - 2.5rem)\" class=\"relative\" v-if=\"webUrl\">\n        <webview\n            ref=\"web\"\n            id=\"web\"\n            :src=\"webUrl\"\n            :useragent=\"webUserAgent\"\n            nodeintegration\n            :preload=\"webPreload\"\n        ></webview>\n        <PageWebviewStatus ref=\"status\" />\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n#web {\n    width: 100%;\n    height: calc(100vh - 2.5rem);\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageGuide.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick, onBeforeMount, onMounted, ref } from \"vue\";\nimport { AppConfig } from \"../config\";\nimport PageWebviewStatus from \"../components/common/PageWebviewStatus.vue\";\n\nconst status = ref<InstanceType<typeof PageWebviewStatus> | null>(null);\nconst web = ref<any | null>(null);\nconst webPreload = ref(\"\");\nconst webUrl = ref(\"\");\nconst webUserAgent = window.$mapi.app.getUserAgent();\n\nonMounted(async () => {\n    webPreload.value = await window.$mapi.app.getPreload();\n    nextTick(() => {\n        web.value.addEventListener(\"did-fail-load\", (event: any) => {\n            status.value?.setStatus(\"fail\");\n        });\n        web.value.addEventListener(\"dom-ready\", () => {\n            // web.value.openDevTools()\n            status.value?.setStatus(\"success\");\n        });\n        status.value?.setStatus(\"loading\");\n        webUrl.value = AppConfig.guideUrl;\n    });\n});\n</script>\n\n<template>\n    <div class=\"pb-guide-container relative\">\n        <div v-if=\"webPreload\">\n            <webview\n                ref=\"web\"\n                id=\"web\"\n                :src=\"webUrl\"\n                :useragent=\"webUserAgent\"\n                nodeintegration\n                :preload=\"webPreload\"\n            ></webview>\n        </div>\n        <PageWebviewStatus ref=\"status\" />\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n.pb-guide-container,\n#web {\n    width: 100%;\n    height: calc(100vh - 2.5rem);\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageLog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport FileLogViewer from \"../components/common/FileLogViewer.vue\";\n\nconst file = ref(\"\");\nconst autoScroll = ref(true);\nconst doOpen = async () => {\n    window.$mapi.app.showItemInFolder(file.value);\n};\nwindow[\"__logInit\"] = (option: { log: string }) => {\n    file.value = option.log;\n};\n</script>\n\n<template>\n    <div style=\"height: calc(100vh - 2.5rem)\" class=\"flex flex-col\">\n        <div class=\"flex p-2 items-center\">\n            <div class=\"mr-2\">\n                <a-checkbox v-model=\"autoScroll\" />\n                {{ $t(\"log.autoScroll\") }}\n            </div>\n            <div class=\"mr-1\">\n                <a-button @click=\"doOpen\" size=\"mini\">\n                    <template #icon>\n                        <icon-file />\n                    </template>\n                    {{ $t(\"log.openFile\") }}\n                </a-button>\n            </div>\n            <div class=\"text-gray-400 text-xs\">\n                {{ file }}\n            </div>\n        </div>\n        <div class=\"flex-grow overflow-hidden relative bg-black\">\n            <FileLogViewer\n                v-if=\"!!file\"\n                :file=\"file\"\n                :is-data-path=\"false\"\n                :auto-scroll=\"autoScroll\"\n            />\n            <div v-else>\n                <div class=\"text-center py-20 text-gray-300\">\n                    <div>\n                        <icon-info-circle class=\"text-5xl\" />\n                    </div>\n                    <div>\n                        {{ $t(\"log.noLogs\") }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/pages/PageMonitor.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from \"vue\";\nimport PageWebviewStatus from \"../components/common/PageWebviewStatus.vue\";\n\nconst status = ref<InstanceType<typeof PageWebviewStatus> | null>(null);\nconst web = ref<any | null>(null);\n\nconst emit = defineEmits({\n    event: (type: string, data: any) => true,\n});\n\nconst webPreload = ref(\"\");\nconst webUrl = ref(\"\");\nconst webUserAgent =\n    \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36\";\nconst pageTitle = ref(\"\");\nconst pageDebugToolsShow = ref(false);\nconst pageOpenDevTools = ref(false);\nconst pageScript = ref(\"\");\nconst pageStatusType = ref<\"info\" | \"success\" | \"error\">(\"info\");\nconst pageStatusMsg = ref(\"\");\nconst pageStatusColor = computed(() => {\n    if (pageStatusType.value === \"info\") {\n        return \"#000\";\n    } else if (pageStatusType.value === \"success\") {\n        return \"#4caf50\";\n    } else if (pageStatusType.value === \"error\") {\n        return \"#f44336\";\n    }\n    return \"#000\";\n});\n\nwindow.__page.registerCallPage(\"MonitorData\", (resolve, reject, payload) => {\n    const { type, data } = payload;\n    if (\"SetTitle\" == type) {\n        pageTitle.value = data.title;\n        emit(\"event\", \"SetTitle\", { title: pageTitle.value });\n    } else if (\"LoadUrl\" == type) {\n        status.value?.setStatus(\"loading\");\n        pageOpenDevTools.value = data.openDevTools;\n        pageScript.value = data.script;\n        webUrl.value = data.url;\n        emit(\"event\", \"SetTitle\", { title: pageTitle.value + \" \" + data.url });\n    }\n    return resolve(undefined);\n});\n\nconst doOpenWebDevTools = () => {\n    if (web.value) {\n        if (web.value.isDevToolsOpened()) {\n            web.value.closeDevTools();\n        } else {\n            web.value.openDevTools();\n        }\n    }\n};\n\nconst doRefresh = (e) => {\n    if (e.shiftKey) {\n        pageDebugToolsShow.value = !pageDebugToolsShow.value;\n        return;\n    }\n    if (web.value) {\n        web.value.reload();\n    }\n};\n\nwatch(web, (newVal) => {\n    if (!newVal) {\n        return;\n    }\n    console.log(\"webview.listen\", newVal);\n    web.value.addEventListener(\"did-fail-load\", (event: any) => {\n        status.value?.setStatus(\"fail\");\n    });\n    web.value.addEventListener(\"did-finish-load\", (event: any) => {\n        console.log(\"did-finish-load\", event);\n    });\n    web.value.addEventListener(\"close\", (event: any) => {\n        if (web.value.isDevToolsOpened()) {\n            web.value.closeDevTools();\n        }\n    });\n    web.value.addEventListener(\"dom-ready\", (e) => {\n        if (pageOpenDevTools.value) {\n            web.value.openDevTools();\n        }\n        if (pageScript.value) {\n            window.$mapi.user\n                .apiPost(pageScript.value, {}, { throwException: false })\n                .then((res) => {\n                    if (res.code) {\n                        pageStatusMsg.value = `ERROR: ${res.msg}`;\n                    } else {\n                        if (res.data.script) {\n                            // console.log('monitor script', res.data.script)\n                            web.value.executeJavaScript(\n                                `console.log('monitor script run');${res.data.script};`,\n                            );\n                        }\n                    }\n                });\n        }\n        status.value?.setStatus(\"success\");\n    });\n    web.value.addEventListener(\"ipc-message\", (event) => {\n        if (\"data\" === event.channel) {\n            const { type, data } = event.args[0];\n            console.log(\"message\", { type, data });\n            if (\"status\" === type) {\n                pageStatusType.value = data.type;\n                pageStatusMsg.value = data.msg;\n            } else if (\"event\" === type) {\n                pageStatusType.value = \"info\";\n                pageStatusMsg.value = JSON.stringify(data);\n                window.__page.ipcSend(\"MonitorEvent\", data.type, data.data);\n            }\n        }\n    });\n});\n\nonMounted(async () => {\n    webPreload.value = await window.$mapi.app.getPreload();\n});\n</script>\n\n<template>\n    <div class=\"pb-monitor-container relative\">\n        <div class=\"p-2 flex h-12 items-center overflow-hidden\">\n            <a-button\n                shape=\"round\"\n                type=\"primary\"\n                status=\"danger\"\n                class=\"mr-1\"\n                v-if=\"pageDebugToolsShow\"\n                @click=\"doOpenWebDevTools\"\n            >\n                {{ $t(\"monitor.debug\") }}\n            </a-button>\n            <a-button shape=\"round\" type=\"primary\" @click=\"doRefresh\">\n                {{ $t(\"monitor.refresh\") }}\n            </a-button>\n            <div class=\"ml-2\">\n                <div :style=\"{ color: pageStatusColor }\">\n                    {{ pageStatusMsg }}\n                </div>\n            </div>\n        </div>\n        <div>\n            <webview\n                ref=\"web\"\n                :src=\"webUrl\"\n                nodeintegration\n                webpreferences=\"contextIsolation=false,sandbox=false\"\n                partition=\"persist:monitor\"\n                :useragent=\"webUserAgent\"\n                :preload=\"webPreload\"\n                class=\"pb-monitor-web\"\n            ></webview>\n        </div>\n        <PageWebviewStatus ref=\"status\" />\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n.pb-monitor-container,\n.pb-monitor-web {\n    width: 100%;\n    height: calc(100vh - 2.5rem - 3rem);\n}\n</style>\n"
  },
  {
    "path": "src/pages/PagePayment.vue",
    "content": "<script setup lang=\"ts\">\nimport { ipcRenderer } from \"electron\";\nimport QRCode from \"qrcode\";\nimport { onBeforeUnmount, onMounted, ref } from \"vue\";\n\nconst payUrl = ref<string>(\"\");\nconst qrcodeExpireTime = ref<number>(0);\nconst status = ref<\"\" | \"WaitPay\" | \"Scanned\" | \"Payed\" | \"Expired\" | \"Error\">(\n    \"\",\n);\nconst qrcodeExpireLeft = ref<number>(0);\nconst qrcodeUrl = ref<string>(\"\");\nconst body = ref<string>(\"\");\n\nlet countDownTimer = null as any;\nlet watchTimer = null as any;\n\nonMounted(async () => {\n    await doRefresh();\n    countDownTimer = setInterval(() => {\n        if (status.value !== \"WaitPay\") {\n            return;\n        }\n        const left = Math.floor(qrcodeExpireTime.value - Date.now() / 1000);\n        if (left <= 0) {\n            clearInterval(countDownTimer);\n            status.value = \"Expired\";\n            return;\n        }\n        if (left < 60) {\n            qrcodeExpireLeft.value = left;\n        }\n    }, 1000);\n});\n\nonBeforeUnmount(() => {\n    clearInterval(countDownTimer);\n    clearTimeout(watchTimer);\n});\n\nconst doRefresh = async () => {\n    const result = await ipcRenderer.invoke(\"Payment.Event\", \"refresh\");\n    status.value = \"WaitPay\";\n    body.value = result.body;\n    qrcodeExpireTime.value =\n        (Date.now() + 1000 * result.payExpireSeconds) / 1000;\n    qrcodeExpireLeft.value = 0;\n    payUrl.value = result.payUrl;\n    qrcodeUrl.value = await QRCode.toDataURL(payUrl.value, {\n        width: 300,\n        height: 300,\n    });\n    if (watchTimer) {\n        clearTimeout(watchTimer);\n    }\n    doStartWatch().then();\n};\n\nconst doStartWatch = async () => {\n    const result = await ipcRenderer.invoke(\"Payment.Event\", \"watch\");\n    status.value = result.status;\n    switch (result.status) {\n        case \"Scanned\":\n        case \"WaitPay\":\n            watchTimer = setTimeout(() => {\n                doStartWatch().then();\n            }, 3000);\n            break;\n    }\n};\n</script>\n\n<template>\n    <div style=\"height: calc(100vh - 40px)\" class=\"h-full relative\">\n        <div class=\"text-center pt-10\">\n            <div\n                class=\"h-36 w-36 mx-auto rounded shadow-lg overflow-hidden border border-solid border-gray-200 relative\"\n            >\n                <img\n                    class=\"h-36 w-36 rounded\"\n                    v-if=\"qrcodeUrl\"\n                    :src=\"qrcodeUrl\"\n                />\n                <div\n                    v-if=\"status === 'Expired'\"\n                    class=\"inset-0 bg-gray-900 absolute rounded bg-opacity-50 flex\"\n                >\n                    <div class=\"m-auto\">\n                        <div class=\"text-white pb-3\">\n                            <icon-info-circle />\n                            {{ $t(\"payment.qrcodeExpired\") }}\n                        </div>\n                        <div>\n                            <a-button size=\"mini\">\n                                <template #icon>\n                                    <icon-refresh />\n                                </template>\n                                {{ $t(\"common.refresh\") }}\n                            </a-button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"pt-4 font-bold text-lg h-14\">\n                {{ body }}\n            </div>\n            <div class=\"pt-5 w-48 mx-auto h-14\">\n                <div v-if=\"status\">\n                    <div v-if=\"status === 'WaitPay' && qrcodeExpireLeft\">\n                        {{\n                            $t(\"payment.payWithinSeconds\", {\n                                seconds: qrcodeExpireLeft,\n                            })\n                        }}\n                    </div>\n                    <div\n                        v-else-if=\"status === 'Scanned'\"\n                        class=\"text-green-500\"\n                    >\n                        <icon-check /> {{ $t(\"payment.scanned\") }}\n                    </div>\n                    <div v-else-if=\"status === 'Payed'\" class=\"text-green-500\">\n                        <icon-check /> {{ $t(\"payment.paidClosing\") }}\n                    </div>\n                    <div v-else-if=\"status === 'Error'\" class=\"text-red-500\">\n                        {{ $t(\"payment.error\") }}\n                    </div>\n                    <div v-else-if=\"status === 'Expired'\" class=\"text-red-500\">\n                        {{ $t(\"payment.expired\") }}\n                    </div>\n                </div>\n            </div>\n            <div class=\"pt-5\">\n                <icon-qrcode />\n                {{ $t(\"payment.scanQRCode\") }}\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/PageSetup.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, ref } from \"vue\";\nimport { t } from \"../lang\";\nimport { Dialog } from \"../lib/dialog\";\n\nconst recordActiveIndex = ref(0);\nconst recordActive = computed(() => {\n    return records.value[recordActiveIndex.value] || null;\n});\nconst records = ref<\n    {\n        name: string;\n        title: string;\n        status: \"success\" | \"fail\";\n        desc: string;\n        steps: {\n            title: string;\n            image: string;\n        }[];\n    }[]\n>([]);\n\nonMounted(() => {\n    doLoad().then();\n    window.$mapi.app.windowHide(\"main\");\n});\n\nconst doLoad = async () => {\n    records.value = await window.$mapi.app.setupList();\n};\n\nconst doOpen = async () => {\n    if (!recordActive.value) {\n        return;\n    }\n    window.$mapi.app.setupOpen(recordActive.value.name).then();\n};\n\nconst doCheck = async () => {\n    await doLoad();\n    if (records.value[recordActiveIndex.value].status !== \"success\") {\n        return;\n    }\n    Dialog.tipSuccess(\n        t(\"setup.congratulations\", {\n            title: records.value[recordActiveIndex.value].title,\n        }),\n    );\n    // Auto navigate to next\n    let hasMore = false;\n    for (let i = 0; i < records.value.length; i++) {\n        if (records.value[i].status === \"fail\") {\n            recordActiveIndex.value = i;\n            hasMore = true;\n            return;\n        }\n    }\n    if (!hasMore) {\n        await window.$mapi.app.toast(t(\"setup.allCompleted\"));\n        await window.$mapi.app.restart();\n    }\n};\n</script>\n\n<template>\n    <div style=\"height: calc(100vh - 40px)\" class=\"h-full\">\n        <div class=\"h-full flex\">\n            <div class=\"w-48 flex-shrink-0 h-full overflow-auto\">\n                <div v-for=\"(r, rIndex) in records\" class=\"p-2\">\n                    <div\n                        class=\"flex items-start p-2 rounded-lg cursor-pointer hover:bg-gray-100 border\"\n                        @click=\"recordActiveIndex = rIndex\"\n                        :class=\"\n                            rIndex === recordActiveIndex ? 'bg-gray-200' : ''\n                        \"\n                    >\n                        <div class=\"mr-1 pt-3\">\n                            <icon-check-circle\n                                v-if=\"r.status === 'success'\"\n                                class=\"text-green-600 text-lg\"\n                            />\n                            <icon-info-circle\n                                v-else\n                                class=\"text-red-600 text-lg\"\n                            />\n                        </div>\n                        <div>\n                            <div class=\"text-base leading-10\">\n                                {{ r.title }}\n                            </div>\n                            <div class=\"text-xs text-gray-600\">\n                                {{ r.desc }}\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"flex-grow h-full border-l p-3 overflow-auto\">\n                <div v-if=\"recordActive\">\n                    <div v-for=\"(s, sIndex) in recordActive.steps\">\n                        <div class=\"flex items-top\">\n                            <a-button\n                                shape=\"round\"\n                                size=\"mini\"\n                                type=\"primary\"\n                                class=\"mr-2\"\n                                >{{ sIndex + 1 }}</a-button\n                            >\n                            {{ s.title }}\n                        </div>\n                        <div class=\"py-3\">\n                            <img\n                                :src=\"s.image\"\n                                class=\"w-full rounded-lg shadow\"\n                            />\n                        </div>\n                    </div>\n                    <div class=\"h-20\"></div>\n                    <div\n                        class=\"fixed bottom-0 right-0 left-48 bg-white p-3 border\"\n                    >\n                        <div>\n                            <a-button\n                                type=\"primary\"\n                                class=\"mr-2\"\n                                @click=\"doOpen\"\n                            >\n                                <template #icon>\n                                    <icon-settings />\n                                </template>\n                                {{ $t(\"setup.openSettings\") }}\n                            </a-button>\n                            <a-button type=\"primary\" @click=\"doCheck\">\n                                <template #icon>\n                                    <icon-check />\n                                </template>\n                                {{ $t(\"setup.verifyComplete\") }}\n                            </a-button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/PageStore.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from \"vue\";\nimport PageWebviewStatus from \"../components/common/PageWebviewStatus.vue\";\nimport { t } from \"../lang\";\nimport { useSettingStore } from \"../store/modules/setting\";\nimport { useUserStore } from \"../store/modules/user\";\n\nconst setting = useSettingStore();\nconst user = useUserStore();\n\nconst status = ref<InstanceType<typeof PageWebviewStatus> | null>(null);\nconst web = ref<any | null>(null);\nconst webPreload = ref(\"\");\nconst webUrl = ref(\"\");\nconst webUserAgent = window.$mapi.app.getUserAgent();\n\nconst installProgressCallback = (data) => {\n    // console.log('PluginInstallProgress', data)\n    web.value.executeJavaScript(\n        `window.__storePluginInstallProgress && window.__storePluginInstallProgress(${JSON.stringify(data)})`,\n    );\n};\n\nonMounted(async () => {\n    web.value.addEventListener(\"did-fail-load\", (event: any) => {\n        status.value?.setStatus(\"fail\");\n    });\n    web.value.addEventListener(\"dom-ready\", (e) => {\n        // web.value.openDevTools()\n        window.$mapi.user.refresh();\n        web.value.executeJavaScript(`\ndocument.addEventListener('click', (event) => {\n    const target = event.target;\n    if (target.tagName !== 'A') return;\n    const url = target.href\n    if(url.startsWith('javascript:')) return;\n    const urlPath = new URL(url).pathname;\n    event.preventDefault();\n    window.$mapi.user.openWebUrl(url)\n});\n`);\n        status.value?.setStatus(\"success\");\n    });\n    status.value?.setStatus(\"loading\");\n    webPreload.value = await window.$mapi.app.getPreload();\n    webUrl.value = await window.$mapi.user.getWebEnterUrl(`/store`);\n    window.__page.onBroadcast(\"PluginInstallProgress\", installProgressCallback);\n});\n\nonBeforeUnmount(() => {\n    window.__page.offBroadcast(\n        \"PluginInstallProgress\",\n        installProgressCallback,\n    );\n});\n\nfocusany.setSubInput(\n    (keywords) => {\n        web.value.executeJavaScript(\n            `window.__storePluginSearch && window.__storePluginSearch(${JSON.stringify(keywords)});`,\n        );\n    },\n    t(\"store.searchPlaceholder\"),\n    true,\n    true,\n);\n</script>\n\n<template>\n    <div class=\"h-full\">\n        <webview\n            ref=\"web\"\n            id=\"web\"\n            :src=\"webUrl\"\n            :useragent=\"webUserAgent\"\n            nodeintegration\n            :preload=\"webPreload\"\n        ></webview>\n        <PageWebviewStatus ref=\"status\" />\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n#web {\n    width: 100%;\n    height: 100vh;\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageSystem.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { SystemIcons } from \"../../electron/mapi/manager/system/asset/icon\";\nimport SystemAction from \"./System/SystemAction.vue\";\nimport SystemData from \"./System/SystemData.vue\";\nimport SystemFile from \"./System/SystemFile.vue\";\nimport SystemLaunch from \"./System/SystemLaunch.vue\";\nimport SystemPlugin from \"./System/SystemPlugin.vue\";\nimport SystemSetting from \"./System/SystemSetting.vue\";\nimport SystemUser from \"./System/SystemUser.vue\";\nimport SystemAbout from \"./System/SystemAbout.vue\";\nimport SystemModel from \"./System/SystemModel.vue\";\nimport SystemMCP from \"./System/SystemMCP.vue\";\nconst tab = ref(\"setting\");\nwindow.focusany.onPluginReady((data) => {\n    const actionNameMap = {\n        \"page-data\": \"data\",\n        \"page-setting\": \"setting\",\n        \"page-plugin\": \"plugin\",\n        \"page-action\": \"action\",\n        \"page-file\": \"file\",\n        \"page-launch\": \"launch\",\n        \"page-model\": \"model\",\n        \"page-mcp\": \"mcp\",\n    };\n    if (actionNameMap[data.actionName]) {\n        tab.value = actionNameMap[data.actionName];\n    }\n});\n</script>\n\n<template>\n    <div class=\"flex h-dvh border-t border-default\">\n        <div\n            class=\"w-48 flex-shrink-0 border-r border-solid border-default h-full p-3 overflow-y-auto select-none\"\n        >\n            <div class=\"text-gray-600 dark:text-gray-300 pb-4 px-4 py-4\">\n                {{ $t(\"偏好设置\") }}\n            </div>\n            <div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'setting'\"\n                    :class=\"\n                        tab === 'setting'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.pluginSystem\"\n                    />\n                    功能设置\n                </div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'plugin'\"\n                    :class=\"\n                        tab === 'plugin'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.plugin\"\n                    />\n                    插件管理\n                </div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'action'\"\n                    :class=\"\n                        tab === 'action'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.command\"\n                    />\n                    动作管理\n                </div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'file'\"\n                    :class=\"\n                        tab === 'file'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.folder\"\n                    />\n                    文件启动\n                </div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'launch'\"\n                    :class=\"\n                        tab === 'launch'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.thunder\"\n                    />\n                    快捷键\n                </div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'model'\"\n                    :class=\"\n                        tab === 'model'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.model\"\n                    />\n                    AI模型\n                </div>\n                <div\n                    class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                    @click=\"tab = 'mcp'\"\n                    :class=\"\n                        tab === 'mcp'\n                            ? 'bg-gray-200 dark:bg-gray-500'\n                            : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                    \"\n                >\n                    <img\n                        class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                        :src=\"SystemIcons.mcp\"\n                    />\n                    MCP\n                </div>\n                <div class=\"text-gray-600 dark:text-gray-300 pb-4 px-4 py-4\">\n                    {{ $t(\"个人中心\") }}\n                </div>\n                <div>\n                    <div\n                        class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                        @click=\"tab = 'data'\"\n                        :class=\"\n                            tab === 'data'\n                                ? 'bg-gray-200 dark:bg-gray-500'\n                                : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                        \"\n                    >\n                        <img\n                            class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                            :src=\"SystemIcons.database\"\n                        />\n                        数据中心\n                    </div>\n                    <div\n                        class=\"flex items-center leading-10 py-1 px-1 rounded-lg cursor-pointer\"\n                        @click=\"tab = 'about'\"\n                        :class=\"\n                            tab === 'about'\n                                ? 'bg-gray-200 dark:bg-gray-500'\n                                : 'hover:bg-gray-100 dark:hover:bg-gray-600'\n                        \"\n                    >\n                        <img\n                            class=\"w-6 h-6 object-contain mr-2 ml-2 dark:invert\"\n                            :src=\"SystemIcons.about\"\n                        />\n                        关于我们\n                    </div>\n                </div>\n            </div>\n        </div>\n        <div class=\"flex-grow overflow-y-auto\">\n            <SystemUser v-if=\"tab === 'user'\" />\n            <SystemData v-else-if=\"tab === 'data'\" />\n            <SystemSetting v-else-if=\"tab === 'setting'\" />\n            <SystemPlugin v-else-if=\"tab === 'plugin'\" />\n            <SystemAction v-else-if=\"tab === 'action'\" />\n            <SystemFile v-else-if=\"tab === 'file'\" />\n            <SystemLaunch v-else-if=\"tab === 'launch'\" />\n            <SystemModel v-else-if=\"tab === 'model'\" />\n            <SystemMCP v-else-if=\"tab === 'mcp'\" />\n        </div>\n    </div>\n</template>\n\n<style scoped lang=\"less\"></style>\n"
  },
  {
    "path": "src/pages/PageUser.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport PageWebviewStatus from \"../components/common/PageWebviewStatus.vue\";\nimport { useUserPage } from \"../hooks/user\";\n\nconst status = ref<InstanceType<typeof PageWebviewStatus> | null>(null);\nconst web = ref<any | null>(null);\n\nconst { webPreload, webUrl, webUserAgent, user, canGoBack, doBack, onMount } =\n    useUserPage({ web, status });\n\nonMounted(async () => {\n    await onMount();\n});\n</script>\n\n<template>\n    <div class=\"pb-user-container relative\">\n        <div>\n            <webview\n                ref=\"web\"\n                :src=\"webUrl\"\n                nodeintegration\n                :useragent=\"webUserAgent\"\n                :preload=\"webPreload\"\n                class=\"pb-user-web\"\n            ></webview>\n            <div class=\"absolute left-5 top-5 z-40\">\n                <a-button\n                    v-if=\"canGoBack\"\n                    @click=\"doBack\"\n                    type=\"secondary\"\n                    shape=\"round\"\n                >\n                    <template #icon>\n                        <icon-left />\n                    </template>\n                    {{ $t(\"common.back\") }}\n                </a-button>\n            </div>\n        </div>\n        <PageWebviewStatus ref=\"status\" />\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n.pb-user-container,\n.pb-user-web {\n    width: 100%;\n    height: calc(100vh - 2.5rem);\n}\n</style>\n"
  },
  {
    "path": "src/pages/PageWorkflow.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\n\nconst tab = ref(\"list\");\n\nconst drawerVisible = ref(false);\nconst modelVisible = ref(false);\nconst onClose = () => {\n    drawerVisible.value = false;\n};\nconst doModelClose = () => {\n    modelVisible.value = false;\n};\n</script>\n\n<template>\n    <div class=\"h-dvh flex flex-col border-t border-gray-200\">\n        <div\n            class=\"flex items-center p-4 border-b border-solid border-gray-200\"\n        >\n            <div class=\"flex-grow text-2xl\">{{ $t(\"workflow.workflow\") }}</div>\n            <div>\n                <a-button @click=\"tab = 'edit'\" class=\"ml-1\">\n                    <template #icon>\n                        <icon-plus />\n                    </template>\n                    {{ $t(\"workflow.create\") }}\n                </a-button>\n                <a-button @click=\"tab = 'list'\" class=\"ml-1\">\n                    <template #icon>\n                        <icon-save />\n                    </template>\n                    {{ $t(\"common.save\") }}\n                </a-button>\n            </div>\n        </div>\n        <div class=\"flex-grow overflow-y-auto\">\n            <!--            <WorkflowList v-if=\"tab==='list'\"/>-->\n            <!--            <WorkflowEdit v-else-if=\"tab==='edit'\"/>-->\n        </div>\n    </div>\n</template>\n\n<style scoped lang=\"less\"></style>\n"
  },
  {
    "path": "src/pages/Setting.vue",
    "content": "<template></template>\n"
  },
  {
    "path": "src/pages/System/SystemAbout.vue",
    "content": "<script setup lang=\"ts\">\nimport { AppConfig } from \"../../config\";\nimport { t } from \"../../lang\";\nimport UpdaterButton from \"../../components/common/UpdaterButton.vue\";\nimport { useSettingStore } from \"../../store/modules/setting\";\nimport FeedbackTicketButton from \"../../components/common/FeedbackTicketButton.vue\";\n\nconst setting = useSettingStore();\nconst licenseYear = new Date().getFullYear();\n\nconst doOpenLog = async () => {\n    await window.$mapi.file.openPath(window.$mapi.log.root());\n};\n</script>\n\n<template>\n    <div class=\"p-4\">\n        <div class=\"flex items-center\">\n            <div class=\"flex-grow text-2xl\">关于我们</div>\n        </div>\n        <div class=\"pt-4\">\n            <div class=\"flex mb-3 items-start\">\n                <div class=\"w-20\">{{ t(\"版本\") }}</div>\n                <div class=\"flex-grow\">\n                    <div>\n                        v{{ AppConfig.version }} Build\n                        {{ setting.buildInfo.buildId }}\n                    </div>\n                    <div class=\"pt-2\">\n                        <UpdaterButton />\n                    </div>\n                </div>\n            </div>\n            <div class=\"flex mb-3 items-center\">\n                <div class=\"w-20\">{{ t(\"官网\") }}</div>\n                <div class=\"flex-grow\">\n                    <a\n                        :href=\"AppConfig.website\"\n                        target=\"_blank\"\n                        class=\"text-link\"\n                    >\n                        {{ AppConfig.website }}\n                    </a>\n                    <div class=\"inline-block ml-3\">\n                        <FeedbackTicketButton />\n                    </div>\n                    <a-button class=\"ml-3\" size=\"mini\" @click=\"doOpenLog\">\n                        <template #icon>\n                            <icon-file />\n                        </template>\n                        {{ t(\"日志\") }}\n                    </a-button>\n                </div>\n            </div>\n            <div class=\"flex mb-3 items-center\">\n                <div class=\"w-20\">{{ t(\"声明\") }}</div>\n                <div class=\"flex-grow\">\n                    {{ t(\"本产品为开源软件，遵循 Apache 2.0 license 协议。\") }}\n                </div>\n            </div>\n            <div class=\"mb-3\">\n                <a\n                    :href=\"AppConfig.websiteGithub\"\n                    target=\"_blank\"\n                    class=\"bg-gray-100 dark:bg-gray-700 w-48 mr-1 rounded-lg py-2 px-8 inline-flex items-center mb-3 hover:shadow-lg\"\n                >\n                    <img\n                        src=\"./../../assets/image/github.svg\"\n                        class=\"w-12 h-12 mr-2\"\n                    />\n                    <div class=\"flex-grow\">Github</div>\n                </a>\n                <a\n                    :href=\"AppConfig.websiteGitee\"\n                    target=\"_blank\"\n                    class=\"bg-gray-100 dark:bg-gray-700 w-48 mr-1 rounded-lg py-2 px-8 inline-flex items-center hover:shadow-lg\"\n                >\n                    <img\n                        src=\"./../../assets/image/gitee.svg\"\n                        class=\"w-12 h-12 mr-2\"\n                    />\n                    <div class=\"flex-grow\">Gitee</div>\n                </a>\n            </div>\n            <div class=\"text-gray-400\">\n                &copy; {{ licenseYear }} {{ AppConfig.name }}\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemAction.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport IconPin from \"~icons/mdi/pin\";\nimport { SystemIcons } from \"../../../electron/mapi/manager/system/asset/icon\";\nimport MEmpty from \"../../components/common/MEmpty.vue\";\nimport { t } from \"../../lang\";\nimport { Dialog } from \"../../lib/dialog\";\nimport {\n    ActionMatchBase,\n    ActionMatchEditor,\n    ActionMatchKey,\n    ActionMatchRegex,\n    ActionMatchText,\n    ActionMatchWindow,\n    ActionRecord,\n    PluginActionRecord,\n    PluginRecord,\n    PluginType,\n} from \"../../types/Manager\";\nimport ActionTypeIcon from \"./components/ActionTypeIcon.vue\";\nimport SystemActionMatchDetailDialog from \"./components/SystemActionMatchDetailDialog.vue\";\n\nconst actionMatchDetailDialog = ref<InstanceType<\n    typeof SystemActionMatchDetailDialog\n> | null>(null);\nconst records = ref<PluginRecord[]>([]);\nconst recordCurrentIndex = ref(-1);\nconst actionTab = ref(\"keyword\");\n\nconst recordCurrent = computed(() => {\n    if (\n        recordCurrentIndex.value >= 0 &&\n        recordCurrentIndex.value < records.value.length\n    ) {\n        return records.value[recordCurrentIndex.value];\n    }\n    return null;\n});\nconst currentPluginActionsKeywordList = computed(() => {\n    return (\n        recordCurrent.value?.actions.filter((a) => {\n            return (\n                a.matches.filter((m) => {\n                    return [\"text\", \"key\"].includes(\n                        (m as ActionMatchBase).type,\n                    );\n                }).length > 0\n            );\n        }) || []\n    );\n});\nconst currentPluginActionsMatchList = computed(() => {\n    return (\n        recordCurrent.value?.actions.filter((a) => {\n            return (\n                a.matches.filter((m) => {\n                    return ![\"text\", \"key\"].includes(\n                        (m as ActionMatchBase).type,\n                    );\n                }).length > 0\n            );\n        }) || []\n    );\n});\nconst disabledPluginActionMatches = ref<\n    Record<string, Record<string, string[]>>\n>({});\nconst pinPluginAction = ref<PluginActionRecord[]>([]);\nconst doLoad = async () => {\n    const plugins = await window.$mapi.manager.listPlugin();\n    for (const p of plugins) {\n        for (const a of p.actions) {\n            for (const m of a.matches) {\n                m[\"_disable\"] = ((pName, aName, mName) => {\n                    return computed(() => {\n                        return disabledPluginActionMatches.value[pName]?.[\n                            aName\n                        ]?.includes(mName);\n                    });\n                })(p.name, a.name, m.name);\n            }\n            a[\"_pin\"] = ((pName, aName) => {\n                return computed(() => {\n                    return !!pinPluginAction.value.find(\n                        (pa) =>\n                            pa.pluginName === pName && pa.actionName === aName,\n                    );\n                });\n            })(p.name, a.name);\n        }\n    }\n    records.value = plugins;\n    recordCurrentIndex.value = -1;\n    await nextTick(() => {\n        if (records.value.length > 0) {\n            recordCurrentIndex.value = 0;\n        }\n    });\n};\nonMounted(async () => {\n    disabledPluginActionMatches.value =\n        await window.$mapi.manager.listDisabledActionMatch();\n    pinPluginAction.value = await window.$mapi.manager.listPinAction();\n    // console.log('disabledPluginActionMatches', disabledPluginActionMatches.value)\n    // console.log('pinPluginAction', pinPluginAction.value)\n    await doLoad();\n});\nconst doActivePlugin = (index: number) => {\n    actionTab.value = \"keyword\";\n    recordCurrentIndex.value = index;\n};\nconst doDisable = async (action: ActionRecord, matchName: string) => {\n    const disabled = await window.$mapi.manager.toggleDisabledActionMatch(\n        recordCurrent.value?.name as string,\n        action.name,\n        matchName,\n    );\n    disabledPluginActionMatches.value =\n        await window.$mapi.manager.listDisabledActionMatch();\n    // console.log('doDisable', action, matchName)\n    // console.log('disabledPluginActionMatches', JSON.stringify(disabledPluginActionMatches.value, null, 2))\n    if (disabled) {\n        Dialog.tipSuccess(t(\"plugin.disabled\"));\n    } else {\n        Dialog.tipSuccess(t(\"plugin.enabled\"));\n    }\n};\nconst doOpen = async (action: ActionRecord) => {\n    action = JSON.parse(JSON.stringify(action));\n    await window.$mapi.manager.openAction(action);\n};\nconst doPin = async (action: ActionRecord) => {\n    await window.$mapi.manager.togglePinAction(\n        recordCurrent.value?.name as string,\n        action.name,\n    );\n    pinPluginAction.value = await window.$mapi.manager.listPinAction();\n    // console.log('pinPluginAction', JSON.stringify(pinPluginAction.value, null, 2))\n};\n</script>\n\n<template>\n    <div class=\"flex h-full\">\n        <div\n            class=\"w-64 flex-shrink-0 border-r border-default p-1 h-full overflow-y-auto\"\n        >\n            <div class=\"p-2 text-gray-400 font-bold\">\n                {{ $t(\"action.builtin\") }}\n            </div>\n            <template v-for=\"(p, pIndex) in records\">\n                <div\n                    v-if=\"\n                        ['system', 'store', 'workflow', 'app'].includes(p.name)\n                    \"\n                    class=\"flex items-center rounded-lg cursor-pointer select-none p-2 hover:bg-gray-100 dark:hover:bg-gray-600\"\n                    :class=\"\n                        pIndex === recordCurrentIndex\n                            ? 'bg-gray-200 dark:bg-gray-700'\n                            : ''\n                    \"\n                    @click=\"doActivePlugin(pIndex)\"\n                >\n                    <div class=\"w-7 rounded-lg mr-2\">\n                        <img\n                            :src=\"p.logo\"\n                            :class=\"\n                                p.type === PluginType.SYSTEM\n                                    ? 'dark:invert'\n                                    : 'plugin-logo-filter'\n                            \"\n                        />\n                    </div>\n                    <div class=\"flex-grow w-0 truncate\">\n                        {{ p.title }}\n                    </div>\n                </div>\n            </template>\n            <div class=\"p-2 text-gray-400 font-bold\">\n                {{ $t(\"action.plugin\") }}\n            </div>\n            <template v-for=\"(p, pIndex) in records\">\n                <div\n                    v-if=\"\n                        ![\n                            'system',\n                            'store',\n                            'workflow',\n                            'app',\n                            'file',\n                        ].includes(p.name)\n                    \"\n                    class=\"flex items-center rounded-lg cursor-pointer select-none p-2 hover:bg-gray-100 dark:hover:bg-gray-600\"\n                    :class=\"\n                        pIndex === recordCurrentIndex\n                            ? 'bg-gray-200 dark:bg-gray-700'\n                            : ''\n                    \"\n                    @click=\"doActivePlugin(pIndex)\"\n                >\n                    <div class=\"w-8 rounded-lg mr-2\">\n                        <img :src=\"p.logo\" />\n                    </div>\n                    <div class=\"flex-grow w-0 truncate\">\n                        {{ p.title }}\n                    </div>\n                </div>\n            </template>\n        </div>\n        <div class=\"flex-grow h-full overflow-y-auto p-4\">\n            <div v-if=\"recordCurrent\">\n                <div class=\"text-center pb-4\">\n                    <a-radio-group\n                        type=\"button\"\n                        size=\"large\"\n                        v-model=\"actionTab\"\n                    >\n                        <a-radio value=\"keyword\">\n                            <div class=\"flex items-center\">\n                                <img\n                                    class=\"w-6 h-6 mr-1 object-contain dark:invert\"\n                                    :src=\"SystemIcons.searchKeyword\"\n                                />\n                                {{ $t(\"action.searchAction\") }}\n                            </div>\n                        </a-radio>\n                        <a-radio value=\"match\">\n                            <div class=\"flex items-center\">\n                                <img\n                                    class=\"w-6 h-6 mr-1 object-contain dark:invert\"\n                                    :src=\"SystemIcons.searchMatch\"\n                                />\n                                {{ $t(\"action.matchAction\") }}\n                            </div>\n                        </a-radio>\n                    </a-radio-group>\n                </div>\n                <div v-if=\"actionTab === 'keyword'\">\n                    <m-empty v-if=\"!currentPluginActionsKeywordList.length\" />\n                    <div\n                        v-for=\"a in currentPluginActionsKeywordList\"\n                        class=\"py-2\"\n                    >\n                        <div class=\"mb-4 flex items-center\">\n                            <img\n                                class=\"w-6 h-6 object-contain mr-2\"\n                                :class=\"\n                                    recordCurrent?.type === PluginType.SYSTEM\n                                        ? 'dark:invert'\n                                        : 'plugin-logo-filter'\n                                \"\n                                :src=\"a.icon\"\n                            />\n                            <div class=\"mr-2\">{{ a.title }}</div>\n                            <ActionTypeIcon class=\"mr-2\" :type=\"a.type\" />\n                            <a-tooltip\n                                :content=\"\n                                    a['_pin']\n                                        ? $t('action.pinToSearch')\n                                        : $t('action.unpinFromSearch')\n                                \"\n                                position=\"left\"\n                            >\n                                <a\n                                    href=\"javascript:;\"\n                                    class=\"inline-block w-6 h-6 mr-2 bg-gray-100 dark:bg-gray-600 text-center leading-6 rounded-full\"\n                                    :class=\"\n                                        a['_pin']\n                                            ? 'bg-gray-600 dark:bg-gray-200 text-white dark:text-black'\n                                            : ''\n                                    \"\n                                    @click=\"doPin(a as any)\"\n                                >\n                                    <IconPin />\n                                </a>\n                            </a-tooltip>\n                        </div>\n                        <div\n                            v-for=\"m in a.matches.filter((m) =>\n                                ['text', 'key'].includes(\n                                    (m as ActionMatchBase).type,\n                                ),\n                            )\"\n                            class=\"mr-1 mb-1 inline-block\"\n                        >\n                            <a-dropdown>\n                                <a-button-group>\n                                    <a-button\n                                        v-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'text'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ (m as ActionMatchText).text }}\n                                    </a-button>\n                                    <a-button\n                                        v-else-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'key'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ (m as ActionMatchKey).key }}\n                                    </a-button>\n                                    <a-button\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        <template #icon>\n                                            <icon-down />\n                                        </template>\n                                    </a-button>\n                                </a-button-group>\n                                <template #content>\n                                    <a-doption @click=\"doOpen(a as any)\">\n                                        {{ $t(\"common.open\") }}\n                                    </a-doption>\n                                    <a-doption\n                                        v-if=\"m['_disable']\"\n                                        @click=\"\n                                            doDisable(\n                                                a as any,\n                                                m.name as string,\n                                            )\n                                        \"\n                                    >\n                                        {{ $t(\"common.enable\") }}\n                                    </a-doption>\n                                    <a-doption\n                                        v-else\n                                        @click=\"\n                                            doDisable(\n                                                a as any,\n                                                m.name as string,\n                                            )\n                                        \"\n                                    >\n                                        {{ $t(\"common.disable\") }}\n                                    </a-doption>\n                                    <a-doption\n                                        @click=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                    >\n                                        {{ $t(\"common.detail\") }}\n                                    </a-doption>\n                                </template>\n                            </a-dropdown>\n                        </div>\n                    </div>\n                </div>\n                <div v-if=\"actionTab === 'match'\">\n                    <m-empty v-if=\"!currentPluginActionsMatchList.length\" />\n                    <div\n                        v-for=\"a in currentPluginActionsMatchList\"\n                        class=\"py-2\"\n                    >\n                        <div class=\"mb-4 flex items-center\">\n                            <img\n                                class=\"w-6 h-6 object-contain mr-2\"\n                                :src=\"a.icon\"\n                            />\n                            <div class=\"mr-2\">{{ a.title }}</div>\n                            <ActionTypeIcon class=\"mr-2\" :type=\"a.type\" />\n                        </div>\n                        <div\n                            v-for=\"m in a.matches.filter((m) =>\n                                [\n                                    'image',\n                                    'file',\n                                    'regex',\n                                    'window',\n                                    'editor',\n                                ].includes((m as ActionMatchBase).type),\n                            )\"\n                            class=\"mr-1 mb-1 inline-block\"\n                        >\n                            <a-dropdown>\n                                <a-button-group>\n                                    <a-button\n                                        v-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'regex'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ $t(\"action.regex\") }}\n                                        <div\n                                            class=\"inline-block max-w-32 overflow-hidden truncate\"\n                                        >\n                                            {{ (m as ActionMatchRegex).regex }}\n                                        </div>\n                                    </a-button>\n                                    <a-button\n                                        v-else-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'image'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ $t(\"common.image\") }}\n                                    </a-button>\n                                    <a-button\n                                        v-else-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'file'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ $t(\"common.file\") }}\n                                    </a-button>\n                                    <a-button\n                                        v-else-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'window'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ $t(\"action.window\") }}\n                                        <div\n                                            class=\"inline-block max-w-32 overflow-hidden truncate\"\n                                        >\n                                            {{\n                                                (m as ActionMatchWindow)\n                                                    .nameRegex\n                                            }}\n                                            {{\n                                                (m as ActionMatchWindow)\n                                                    .titleRegex\n                                            }}\n                                            {{\n                                                (m as ActionMatchWindow)\n                                                    .attrRegex\n                                            }}\n                                        </div>\n                                    </a-button>\n                                    <a-button\n                                        v-else-if=\"\n                                            (m as ActionMatchBase).type ===\n                                            'editor'\n                                        \"\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        @click.stop=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        {{ $t(\"common.openFile\") }}\n                                        <div\n                                            class=\"inline-block max-w-32 overflow-hidden truncate\"\n                                        >\n                                            <span\n                                                v-if=\"\n                                                    (m as ActionMatchEditor)\n                                                        .fadTypes\n                                                \"\n                                            >\n                                                {{\n                                                    (\n                                                        m as ActionMatchEditor\n                                                    ).fadTypes?.join(\",\")\n                                                }}\n                                            </span>\n                                            <span\n                                                v-if=\"\n                                                    (m as ActionMatchEditor)\n                                                        .extensions\n                                                \"\n                                            >\n                                                {{\n                                                    (\n                                                        m as ActionMatchEditor\n                                                    ).extensions.join(\",\")\n                                                }}\n                                            </span>\n                                        </div>\n                                    </a-button>\n                                    <a-button\n                                        :type=\"\n                                            m['_disable']\n                                                ? undefined\n                                                : 'primary'\n                                        \"\n                                        size=\"small\"\n                                    >\n                                        <template #icon>\n                                            <icon-down />\n                                        </template>\n                                    </a-button>\n                                </a-button-group>\n                                <template #content>\n                                    <a-doption\n                                        v-if=\"m['_disable']\"\n                                        @click=\"\n                                            doDisable(\n                                                a as any,\n                                                m.name as string,\n                                            )\n                                        \"\n                                    >\n                                        {{ $t(\"common.enable\") }}\n                                    </a-doption>\n                                    <a-doption\n                                        v-else\n                                        @click=\"\n                                            doDisable(\n                                                a as any,\n                                                m.name as string,\n                                            )\n                                        \"\n                                    >\n                                        {{ $t(\"common.disable\") }}\n                                    </a-doption>\n                                    <a-doption\n                                        @click=\"\n                                            actionMatchDetailDialog?.show(\n                                                a as any,\n                                                m as any,\n                                            )\n                                        \"\n                                    >\n                                        {{ $t(\"common.detail\") }}\n                                    </a-doption>\n                                </template>\n                            </a-dropdown>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n    <SystemActionMatchDetailDialog\n        ref=\"actionMatchDetailDialog\"\n        @disable=\"doDisable\"\n    />\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemData.vue",
    "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from \"vue\";\nimport { t } from \"../../lang\";\nimport SystemDataBackupDialog from \"./components/SystemDataBackupDialog.vue\";\nimport SystemDataViewDialog from \"./components/SystemDataViewDialog.vue\";\nimport { SystemDataRecord } from \"./components/type\";\n\nconst dataViewDialog = ref<InstanceType<typeof SystemDataViewDialog> | null>(\n    null,\n);\nconst dataBackupDialog = ref<InstanceType<\n    typeof SystemDataBackupDialog\n> | null>(null);\n\nconst records = ref([] as SystemDataRecord[]);\n\nconst doLoad = async () => {\n    records.value = [];\n    const plugins = await window.$mapi.manager.listPlugin();\n    for (const plugin of plugins) {\n        if ([\"store\", \"workflow\", \"app\", \"file\"].includes(plugin.name)) {\n            continue;\n        }\n        const count = await window.$mapi.kvdb.count(plugin.name, \"\");\n        records.value.push({\n            plugin,\n            count,\n        });\n    }\n    return records.value;\n};\n\nonMounted(async () => {\n    await doLoad();\n    window.focusany.setSubInput(\n        (keyword) => {\n            console.log(\"keyword\", keyword);\n        },\n        t(\"data.filterPlaceholder\"),\n        true,\n    );\n});\nonBeforeUnmount(() => {\n    window.focusany.removeSubInput();\n});\n</script>\n\n<template>\n    <div class=\"p-4\">\n        <div class=\"flex items-center\">\n            <div class=\"flex-grow text-2xl\">{{ $t(\"data.title\") }}</div>\n            <div>\n                <a-button size=\"small\" @click=\"dataBackupDialog?.open()\">\n                    {{ $t(\"backup.title\") }}\n                </a-button>\n            </div>\n        </div>\n        <div class=\"mt-3\">\n            <div v-for=\"r in records\">\n                <div class=\"flex py-3 border-t border-default\">\n                    <div\n                        class=\"w-12 bg-gray-100 dark:bg-gray-700 rounded-lg mr-2\"\n                    >\n                        <img :src=\"r.plugin.logo\" />\n                    </div>\n                    <div class=\"flex-grow\">\n                        <div class=\"font-bold\">{{ r.plugin.title }}</div>\n                        <div class=\"text-gray-400\">\n                            {{ r.count }} {{ $t(\"data.docCount\") }}\n                        </div>\n                    </div>\n                    <div>\n                        <div\n                            class=\"w-10 h-10 leading-10 text-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer\"\n                            @click=\"dataViewDialog?.open(r)\"\n                        >\n                            <icon-storage class=\"text-lg\" />\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n    <SystemDataViewDialog ref=\"dataViewDialog\" @update=\"doLoad\" />\n    <SystemDataBackupDialog ref=\"dataBackupDialog\" @update=\"doLoad\" />\n</template>\n\n<style scoped lang=\"less\"></style>\n"
  },
  {
    "path": "src/pages/System/SystemFile.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref, toRaw } from \"vue\";\nimport { SystemIcons } from \"../../../electron/mapi/manager/system/asset/icon\";\nimport DragPasteContainer from \"../../components/common/DragPasteContainer.vue\";\nimport MEmpty from \"../../components/common/MEmpty.vue\";\nimport { FileUtil } from \"../../lib/file\";\nimport { FilePluginRecord } from \"../../types/Manager\";\n\nconst records = ref<FilePluginRecord[]>([]);\n\nonMounted(async () => {\n    await doLoad();\n});\n\nconst doLoad = async () => {\n    records.value = await window.$mapi.manager.listFilePluginRecords();\n};\n\nconst doSave = async () => {\n    await window.$mapi.manager.updateFilePluginRecords(toRaw(records.value));\n};\n\nconst doAdd = async () => {\n    const file = await window.$mapi.file.openFile({\n        properties: [\"openDirectory\", \"openFile\"],\n    });\n    if (!file) {\n        return;\n    }\n    let icon = SystemIcons.folder;\n    if (!(await window.$mapi.file.isDirectory(file, { isDataPath: false }))) {\n        icon = focusany.getFileIcon(file);\n    }\n    let title = FileUtil.getBaseName(file);\n    records.value.push({\n        icon: icon,\n        title: title as string,\n        path: file,\n    });\n    await doSave();\n};\n\nconst doDelete = async (index: number) => {\n    records.value.splice(index, 1);\n    await doSave();\n};\n\nconst onDragDropInput = async (files: any[]) => {\n    // console.log('onDragDropInput', files)\n    for (const f of files) {\n        let icon = SystemIcons.folder;\n        if (f.isFile) {\n            icon = focusany.getFileIcon(f.path);\n        }\n        records.value.push({\n            icon: icon,\n            title: FileUtil.getBaseName(f.name),\n            path: f.path,\n        });\n        await doSave();\n    }\n};\n</script>\n\n<template>\n    <DragPasteContainer @input=\"onDragDropInput\">\n        <div class=\"p-4\">\n            <div class=\"flex items-center\">\n                <div class=\"flex-grow text-2xl\">\n                    {{ $t(\"system.fileLaunch\") }}\n                </div>\n                <div></div>\n                <div>\n                    <a-button\n                        v-if=\"!!records.length\"\n                        size=\"small\"\n                        @click=\"doAdd\"\n                    >\n                        <template #icon>\n                            <icon-plus />\n                        </template>\n                        {{ $t(\"common.add\") }}\n                    </a-button>\n                </div>\n            </div>\n            <div class=\"pt-4\">\n                <m-empty v-if=\"!records.length\" />\n                <div\n                    v-for=\"(r, rIndex) in records\"\n                    class=\"border-t border-solid border-gray-200\"\n                >\n                    <div class=\"flex py-3\">\n                        <div\n                            class=\"w-12 bg-gray-100 rounded-lg mr-2 flex-shrink-0 flex\"\n                        >\n                            <img\n                                :src=\"r.icon\"\n                                class=\"w-10 h-10 object-contain m-auto\"\n                            />\n                        </div>\n                        <div class=\"flex-grow w-0 pr-10\">\n                            <div class=\"font-bold\">{{ r.title }}</div>\n                            <div class=\"text-gray-400 truncate\">\n                                {{ r.path }}\n                            </div>\n                        </div>\n                        <div>\n                            <a-button\n                                type=\"primary\"\n                                status=\"danger\"\n                                @click=\"doDelete(rIndex)\"\n                            >\n                                <template #icon>\n                                    <icon-delete />\n                                </template>\n                            </a-button>\n                        </div>\n                    </div>\n                </div>\n                <div :class=\"records.length > 0 ? '' : 'text-center'\">\n                    <a-button @click=\"doAdd\">\n                        <template #icon>\n                            <icon-plus />\n                        </template>\n                        {{ $t(\"system.addFileLaunch\") }}\n                    </a-button>\n                </div>\n            </div>\n        </div>\n    </DragPasteContainer>\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemLaunch.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref, toRaw } from \"vue\";\nimport { SystemIcons } from \"../../../electron/mapi/manager/system/asset/icon\";\nimport MEmpty from \"../../components/common/MEmpty.vue\";\nimport { t } from \"../../lang\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { LaunchRecord } from \"../../types/Manager\";\nimport HotkeyInput from \"./components/HotkeyInput.vue\";\n\nconst records = ref<LaunchRecord[]>([]);\n\nonMounted(async () => {\n    await doLoad();\n});\n\nconst doLoad = async () => {\n    records.value = await window.$mapi.manager.listLaunchRecords();\n};\n\nconst doSave = async () => {\n    await window.$mapi.manager.updateLaunchRecords(toRaw(records.value));\n    doLoad().then();\n};\n\nconst doAdd = async () => {\n    records.value.push({\n        type: \"custom\",\n        pluginName: \"\",\n        name: \"\",\n        hotkey: null as any,\n        keyword: \"\",\n    });\n    await doSave();\n};\n\nconst doDelete = async (index: number) => {\n    records.value.splice(index, 1);\n    await doSave();\n};\n\nconst doTest = async (index: number) => {\n    const record = records.value[index];\n    if (!record.keyword) {\n        Dialog.tipError(t(\"launch.enterActionName\"));\n        return;\n    }\n    focusany.redirect(record.keyword);\n};\n\nconst doHotkeyChange = async (index: number, hotkey: any) => {\n    records.value[index].hotkey = hotkey;\n    await doSave();\n};\n</script>\n\n<template>\n    <div class=\"p-4\">\n        <div class=\"flex items-center\">\n            <div class=\"flex-grow text-2xl\">{{ $t(\"launch.hotkey\") }}</div>\n            <div>\n                <a-button @click=\"doAdd\">\n                    <template #icon>\n                        <icon-plus />\n                    </template>\n                    {{ $t(\"launch.addHotkey\") }}\n                </a-button>\n            </div>\n        </div>\n        <div class=\"pt-4\">\n            <m-empty v-if=\"!records.length\" />\n            <div\n                v-for=\"(r, rIndex) in records\"\n                class=\"border-t border-solid border-gray-200\"\n            >\n                <div class=\"flex py-3 items-center\">\n                    <div class=\"w-8 flex-shrink-0\">\n                        <a-tooltip\n                            v-if=\"r.type === 'plugin'\"\n                            :content=\"$t('action.plugin') + ':' + r.pluginName\"\n                        >\n                            <img\n                                class=\"w-6 h-6 object-contain dark:invert\"\n                                :src=\"SystemIcons.plugin\"\n                            />\n                        </a-tooltip>\n                        <a-tooltip v-else :content=\"$t('launch.custom')\">\n                            <img\n                                class=\"w-6 h-6 object-contain dark:invert\"\n                                :src=\"SystemIcons.command\"\n                            />\n                        </a-tooltip>\n                    </div>\n                    <div class=\"w-42 flex-shrink-0\">\n                        <HotkeyInput\n                            :value=\"r.hotkey\"\n                            @change=\"doHotkeyChange(rIndex, $event)\"\n                        />\n                    </div>\n                    <div class=\"ml-3 flex-shrink-0 w-28\">\n                        <a-input\n                            v-model=\"r.name\"\n                            :disabled=\"r.type === 'plugin'\"\n                            @change=\"doSave()\"\n                            :placeholder=\"$t('common.description')\"\n                        />\n                    </div>\n                    <div class=\"ml-3 flex-grow\">\n                        <a-input\n                            v-model=\"r.keyword\"\n                            :disabled=\"r.type === 'plugin'\"\n                            @change=\"doSave()\"\n                            :placeholder=\"$t('launch.actionName')\"\n                        />\n                    </div>\n                    <div class=\"ml-2\">\n                        <a-button @click=\"doTest(rIndex)\" class=\"px-3\">\n                            <template #icon>\n                                <icon-play-arrow />\n                            </template>\n                            {{ $t(\"common.test\") }}\n                        </a-button>\n                    </div>\n                    <div class=\"ml-2\">\n                        <a-button\n                            type=\"primary\"\n                            status=\"danger\"\n                            @click=\"doDelete(rIndex)\"\n                        >\n                            <template #icon>\n                                <icon-delete />\n                            </template>\n                        </a-button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemMCP.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport { doCopy } from \"../../components/common/util\";\n\nconst mcpServer = ref(\"...\");\nconst tools = ref<\n    {\n        name: string;\n        description: string;\n    }[]\n>([]);\nonMounted(async () => {\n    mcpServer.value = await window.$mapi.manager.getMcpServer();\n    const mcpInfo = await window.$mapi.manager.getMcpInfo();\n    tools.value = mcpInfo.tools;\n});\n</script>\n\n<template>\n    <div\n        class=\"overflow-x-hidden overflow-y-auto\"\n        style=\"height: calc(100vh - 1px)\"\n    >\n        <div class=\"p-4\">\n            <div class=\"flex items-center mb-4\">\n                <div class=\"flex-grow text-2xl\">MCP</div>\n                <div></div>\n                <div></div>\n            </div>\n            <div class=\"font-bold mb-4\">{{ $t(\"mcp.serverAddress\") }}</div>\n            <div class=\"flex items-center mb-4\">\n                <div\n                    class=\"font-mono mr-2 bg-gray-100 px-2 rounded-lg leading-7\"\n                >\n                    {{ mcpServer }}\n                </div>\n                <div class=\"flex-grow\">\n                    <a-button size=\"mini\" @click=\"doCopy(mcpServer)\">\n                        <template #icon>\n                            <icon-copy />\n                        </template>\n                    </a-button>\n                </div>\n            </div>\n            <div class=\"font-bold mb-4\">MCP Tools</div>\n            <div class=\"mb-4\">\n                <div\n                    v-if=\"tools.length === 0\"\n                    class=\"text-center py-2 text-gray-400 mb-2\"\n                >\n                    <icon-empty />\n                    {{ $t(\"mcp.noTools\") }}\n                </div>\n                <div\n                    v-for=\"tool in tools\"\n                    :key=\"tool.name\"\n                    class=\"mb-2 p-2 border border-gray-300 rounded-lg hover:bg-gray-100\"\n                >\n                    <div class=\"font-bold\">{{ tool.name }}</div>\n                    <div class=\"text-sm text-gray-600\">\n                        {{ tool.description }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemModel.vue",
    "content": "<script setup lang=\"ts\">\nimport ModelSetting from \"../../module/Model/ModelSetting.vue\";\n</script>\n\n<template>\n    <div class=\"overflow-hidden\" style=\"height: calc(100vh - 1px)\">\n        <ModelSetting />\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemPlugin.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref } from \"vue\";\nimport IconPin from \"~icons/mdi/pin\";\nimport { SystemIcons } from \"../../../electron/mapi/manager/system/asset/icon\";\nimport MEmpty from \"../../components/common/MEmpty.vue\";\nimport { t } from \"../../lang\";\nimport { Dialog } from \"../../lib/dialog\";\nimport { mapError } from \"../../lib/error\";\nimport {\n    ActionMatchBase,\n    ActionMatchEditor,\n    ActionMatchKey,\n    ActionMatchRegex,\n    ActionMatchText,\n    ActionMatchWindow,\n    ActionRecord,\n    PluginActionRecord,\n    PluginRecord,\n    PluginType,\n} from \"../../types/Manager\";\nimport ActionTypeIcon from \"./components/ActionTypeIcon.vue\";\nimport SystemActionMatchDetailDialog from \"./components/SystemActionMatchDetailDialog.vue\";\n\nconst actionMatchDetailDialog = ref<InstanceType<\n    typeof SystemActionMatchDetailDialog\n> | null>(null);\nconst records = ref<PluginRecord[]>([]);\nconst recordCurrentIndex = ref(-1);\nconst actionTab = ref(\"keyword\");\nconst filterKeywords = ref(\"\");\n\nconst recordsFilter = computed(() => {\n    return records.value.filter((r) => {\n        if (filterKeywords.value) {\n            const kw = filterKeywords.value.toLowerCase();\n            if (\n                r.title.toLowerCase().includes(kw) ||\n                r.name.toLowerCase().includes(kw) ||\n                r.description?.toLowerCase().includes(kw)\n            ) {\n                return true;\n            }\n            return false;\n        }\n        return ![\"store\", \"workflow\", \"app\", \"file\"].includes(r.name);\n    });\n});\nconst recordCurrent = computed(() => {\n    if (\n        recordCurrentIndex.value >= 0 &&\n        recordCurrentIndex.value < recordsFilter.value.length\n    ) {\n        return recordsFilter.value[recordCurrentIndex.value];\n    }\n    return null;\n});\nconst currentPluginActionsKeywordList = computed(() => {\n    return (\n        recordCurrent.value?.actions.filter((a) => {\n            return (\n                a.matches.filter((m) => {\n                    return [\"text\", \"key\"].includes(\n                        (m as ActionMatchBase).type,\n                    );\n                }).length > 0\n            );\n        }) || []\n    );\n});\nconst currentPluginActionsMatchList = computed(() => {\n    return (\n        recordCurrent.value?.actions.filter((a) => {\n            return (\n                a.matches.filter((m) => {\n                    return ![\"text\", \"key\"].includes(\n                        (m as ActionMatchBase).type,\n                    );\n                }).length > 0\n            );\n        }) || []\n    );\n});\nconst currentPluginMcpList = computed(() => {\n    return recordCurrent.value?.mcp?.tools || [];\n});\nconst disabledPluginActionMatches = ref<\n    Record<string, Record<string, string[]>>\n>({});\nconst pinPluginAction = ref<PluginActionRecord[]>([]);\nconst developerPlugins = ref<string[]>([]);\n\nconst loadDeveloperInfo = async () => {\n    const res = await window.$mapi.user.apiPost(\"store/member\", {});\n    developerPlugins.value = res.data.developerPlugins;\n};\n\nconst doLoad = async () => {\n    const plugins = await window.$mapi.manager.listPlugin();\n    for (const p of plugins) {\n        for (const a of p.actions) {\n            for (const m of a.matches) {\n                m[\"_disable\"] = ((pName, aName, mName) => {\n                    return computed(() => {\n                        return disabledPluginActionMatches.value[pName]?.[\n                            aName\n                        ]?.includes(mName);\n                    });\n                })(p.name, a.name, m.name);\n            }\n            a[\"_pin\"] = ((pName, aName) => {\n                return computed(() => {\n                    return !!pinPluginAction.value.find(\n                        (pa) =>\n                            pa.pluginName === pName && pa.actionName === aName,\n                    );\n                });\n            })(p.name, a.name);\n        }\n    }\n    let currentPluginName = \"\";\n    if (recordCurrentIndex.value >= 0) {\n        currentPluginName = recordsFilter.value[recordCurrentIndex.value].name;\n    }\n    records.value = plugins;\n    recordCurrentIndex.value = -1;\n    await nextTick(() => {\n        if (recordsFilter.value.length > 0) {\n            if (currentPluginName) {\n                const index = recordsFilter.value.findIndex(\n                    (r) => r.name === currentPluginName,\n                );\n                if (index >= 0) {\n                    recordCurrentIndex.value = index;\n                } else {\n                    recordCurrentIndex.value = 0;\n                }\n            } else {\n                recordCurrentIndex.value = 0;\n            }\n        }\n    });\n};\nonMounted(async () => {\n    disabledPluginActionMatches.value =\n        await window.$mapi.manager.listDisabledActionMatch();\n    pinPluginAction.value = await window.$mapi.manager.listPinAction();\n    // console.log('disabledPluginActionMatches', disabledPluginActionMatches.value)\n    // console.log('pinPluginAction', pinPluginAction.value)\n    loadDeveloperInfo().then();\n    await doLoad();\n    focusany.setSubInput(\n        (keywords) => {\n            filterKeywords.value = keywords;\n        },\n        t(\"plugin.search\"),\n        true,\n        true,\n    );\n});\nonBeforeUnmount(() => {\n    focusany.removeSubInput();\n});\n\nconst doActivePlugin = (index: number) => {\n    actionTab.value = \"keyword\";\n    recordCurrentIndex.value = index;\n};\nconst doDisable = async (action: ActionRecord, matchName: string) => {\n    const disabled = await window.$mapi.manager.toggleDisabledActionMatch(\n        recordCurrent.value?.name as string,\n        action.name,\n        matchName,\n    );\n    disabledPluginActionMatches.value =\n        await window.$mapi.manager.listDisabledActionMatch();\n    // console.log('doDisable', action, matchName)\n    // console.log('disabledPluginActionMatches', JSON.stringify(disabledPluginActionMatches.value, null, 2))\n    if (disabled) {\n        Dialog.tipSuccess(t(\"plugin.disabled\"));\n    } else {\n        Dialog.tipSuccess(t(\"plugin.enabled\"));\n    }\n};\nconst doOpen = async (action: ActionRecord) => {\n    action = JSON.parse(JSON.stringify(action));\n    await window.$mapi.manager.openAction(action);\n};\nconst doPin = async (action: ActionRecord) => {\n    await window.$mapi.manager.togglePinAction(\n        recordCurrent.value?.name as string,\n        action.name,\n    );\n    pinPluginAction.value = await window.$mapi.manager.listPinAction();\n    // console.log('pinPluginAction', JSON.stringify(pinPluginAction.value, null, 2))\n};\nconst doUninstall = async () => {\n    await Dialog.confirm(t(\"plugin.uninstallConfirm\"));\n    try {\n        await window.$mapi.manager.uninstallPlugin(\n            recordCurrent.value?.name as string,\n        );\n        Dialog.tipSuccess(t(\"plugin.uninstallSuccess\"));\n        doLoad().then();\n    } catch (e) {\n        Dialog.tipError(t(\"plugin.uninstallFailed\", { error: mapError(e) }));\n    }\n};\nconst doRefreshInstall = async () => {\n    await window.$mapi.manager.refreshInstallPlugin(\n        recordCurrent.value?.name as string,\n    );\n    Dialog.tipSuccess(t(\"plugin.refreshSuccess\"));\n    doLoad().then();\n};\nconst doPublish = async () => {\n    Dialog.loadingOn(t(\"plugin.publishing\"));\n    try {\n        const res = await window.$mapi.manager.storePublish(\n            recordCurrent.value?.name as string,\n            {\n                version: recordCurrent.value?.version as string,\n            },\n        );\n        if (res.code === 0) {\n            Dialog.alertError(t(\"plugin.publishSuccess\"));\n            doLoad().then();\n        } else {\n            Dialog.alertError(t(\"plugin.publishFailed\", { error: res.msg }));\n        }\n    } catch (e) {\n        Dialog.tipError(t(\"plugin.publishFailed\", { error: mapError(e) }));\n    } finally {\n        Dialog.loadingOff();\n    }\n};\nconst doPublishInfo = async () => {\n    Dialog.loadingOn(t(\"plugin.updatingInfo\"));\n    try {\n        await window.$mapi.manager.storePublishInfo(\n            recordCurrent.value?.name as string,\n            {\n                version: recordCurrent.value?.version as string,\n            },\n        );\n        Dialog.tipSuccess(t(\"plugin.updateInfoSuccess\"));\n        doLoad().then();\n    } catch (e) {\n        Dialog.tipError(t(\"plugin.updateInfoFailed\") + \":\" + mapError(e));\n    } finally {\n        Dialog.loadingOff();\n    }\n};\nconst doLog = async () => {\n    await window.$mapi.manager.showLog(recordCurrent.value?.name as string);\n};\nconst doInstallPlugin = async (type: \"zip\" | \"config\") => {\n    const filters: any[] = [];\n    if (\"zip\" === type) {\n        filters.push({ extensions: [\"zip\"] });\n    } else if (\"config\" === type) {\n        filters.push({ name: \"config.json\", extensions: [\"json\"] });\n    }\n    const file = await window.$mapi.file.openFile({\n        filters,\n    });\n    if (!file) {\n        return;\n    }\n    try {\n        await window.$mapi.manager.installPlugin(file);\n        Dialog.tipSuccess(t(\"plugin.installSuccess\"));\n        doLoad().then();\n    } catch (e) {\n        Dialog.tipError(t(\"plugin.installFailed\") + \":\" + mapError(e));\n    }\n};\nconst doInstallStore = async () => {\n    await window.$mapi.manager.openPlugin(\"store\");\n};\n</script>\n\n<template>\n    <div class=\"flex h-full select-none\">\n        <div\n            class=\"w-64 flex-shrink-0 border-r border-default h-full flex flex-col relative\"\n        >\n            <div class=\"flex-grow overflow-y-auto p-1\">\n                <m-empty\n                    v-if=\"!recordsFilter.length\"\n                    :text=\"$t('plugin.notFound')\"\n                />\n                <div\n                    v-for=\"(r, rIndex) in recordsFilter\"\n                    class=\"flex items-center rounded-lg cursor-pointer select-none p-2 hover:bg-gray-100 dark:hover:bg-gray-600 relative\"\n                    @click=\"doActivePlugin(rIndex)\"\n                    :class=\"\n                        recordCurrent?.name === r.name\n                            ? 'bg-gray-200 dark:bg-gray-700'\n                            : ''\n                    \"\n                >\n                    <div class=\"w-7 rounded-lg mr-2\">\n                        <img\n                            :src=\"r.logo\"\n                            :class=\"\n                                r.type === PluginType.SYSTEM\n                                    ? 'dark:invert'\n                                    : 'plugin-logo-filter'\n                            \"\n                        />\n                    </div>\n                    <div class=\"flex-grow w-0 truncate\">\n                        {{ r.title }}\n                    </div>\n                    <div\n                        v-if=\"r.type === 'dir'\"\n                        class=\"text-xs bg-red-100 text-red-600 rounded px-1\"\n                    >\n                        DEV\n                    </div>\n                    <div\n                        v-else-if=\"r.type === 'zip'\"\n                        class=\"text-xs bg-gray-100 text-gray-600 rounded px-1\"\n                    >\n                        ZIP\n                    </div>\n                </div>\n            </div>\n            <div class=\"border-t border-solid border-default py-2 text-center\">\n                <a-dropdown-button\n                    type=\"primary\"\n                    @click=\"doInstallStore\"\n                    class=\"block\"\n                >\n                    <icon-apps class=\"mr-1\" />\n                    {{ $t(\"plugin.market\") }}\n                    <template #content>\n                        <a-doption @click=\"doInstallPlugin('zip')\">{{\n                            $t(\"plugin.installLocalZip\")\n                        }}</a-doption>\n                        <a-doption @click=\"doInstallPlugin('config')\">{{\n                            $t(\"plugin.installLocalConfig\")\n                        }}</a-doption>\n                    </template>\n                </a-dropdown-button>\n            </div>\n        </div>\n        <div class=\"flex-grow h-full overflow-y-auto p-4\" v-if=\"recordCurrent\">\n            <div class=\"flex items-center\">\n                <div class=\"w-12 rounded-lg mr-2\">\n                    <img\n                        :src=\"recordCurrent.logo\"\n                        :class=\"\n                            recordCurrent.type === PluginType.SYSTEM\n                                ? 'dark:invert'\n                                : 'plugin-logo-filter'\n                        \"\n                    />\n                </div>\n                <div class=\"flex-grow w-0 truncate\">\n                    <div class=\"leading-6 flex items-center\">\n                        <div class=\"font-bold mr-2\">\n                            {{ recordCurrent.title }}\n                        </div>\n                        <div class=\"flex-grow\"></div>\n                    </div>\n                    <div class=\"flex items-center mb-1\">\n                        <div\n                            class=\"text-gray-400 text-xs mr-2\"\n                            v-if=\"recordCurrent.type !== PluginType.SYSTEM\"\n                        >\n                            {{ recordCurrent.name }}\n                        </div>\n                        <div\n                            class=\"text-gray-400 text-xs mr-2\"\n                            v-if=\"recordCurrent.type !== PluginType.SYSTEM\"\n                        >\n                            v{{ recordCurrent.version }}\n                        </div>\n                        <a-tooltip\n                            :content=\"\n                                $t('plugin.localPlugin', {\n                                    path: recordCurrent.runtime?.root,\n                                })\n                            \"\n                        >\n                            <div\n                                v-if=\"recordCurrent.type === 'dir'\"\n                                class=\"text-xs bg-red-100 text-red-600 rounded px-1 cursor-pointer\"\n                            >\n                                DEV\n                            </div>\n                        </a-tooltip>\n                        <div class=\"flex-grow\"></div>\n                    </div>\n                    <div class=\"text-gray-400 text-sm w-full truncate\">\n                        {{ recordCurrent.description }}\n                    </div>\n                </div>\n                <div class=\"ml-3 flex items-center\">\n                    <a-button\n                        v-if=\"recordCurrent.name !== 'system'\"\n                        type=\"primary\"\n                        class=\"ml-1\"\n                        status=\"danger\"\n                        size=\"small\"\n                        @click=\"doUninstall\"\n                    >\n                        {{ $t(\"plugin.uninstall\") }}\n                    </a-button>\n                    <a-dropdown\n                        v-if=\"\n                            recordCurrent.runtime &&\n                            recordCurrent.type === PluginType.DIR\n                        \"\n                    >\n                        <a-button size=\"small\" class=\"ml-1\">\n                            <template #icon>\n                                <icon-down />\n                            </template>\n                        </a-button>\n                        <template #content>\n                            <a-doption @click=\"doRefreshInstall\">{{\n                                $t(\"common.refresh\")\n                            }}</a-doption>\n                            <a-doption\n                                v-if=\"\n                                    developerPlugins.includes(\n                                        recordCurrent.name,\n                                    )\n                                \"\n                                @click=\"doPublish\"\n                            >\n                                {{ $t(\"plugin.publish\") }}\n                            </a-doption>\n                            <a-doption\n                                v-if=\"\n                                    developerPlugins.includes(\n                                        recordCurrent.name,\n                                    )\n                                \"\n                                @click=\"doPublishInfo\"\n                            >\n                                {{ $t(\"plugin.updateInfo\") }}\n                            </a-doption>\n                            <a-doption\n                                v-if=\"\n                                    developerPlugins.includes(\n                                        recordCurrent.name,\n                                    )\n                                \"\n                                @click=\"doLog\"\n                            >\n                                {{ $t(\"nav.log\") }}\n                            </a-doption>\n                        </template>\n                    </a-dropdown>\n                </div>\n            </div>\n            <!--             <pre>{{JSON.stringify(developerPlugins,null,2)}}</pre>-->\n            <div class=\"border-default border-t my-4\"></div>\n            <div class=\"text-center pb-4\">\n                <a-radio-group type=\"button\" size=\"large\" v-model=\"actionTab\">\n                    <a-radio value=\"keyword\">\n                        <div class=\"flex items-center\">\n                            <img\n                                class=\"w-6 h-6 mr-1 object-contain dark:invert\"\n                                :src=\"SystemIcons.searchKeyword\"\n                            />\n                            {{ $t(\"action.searchAction\") }}\n                        </div>\n                    </a-radio>\n                    <a-radio value=\"match\">\n                        <div class=\"flex items-center\">\n                            <img\n                                class=\"w-6 h-6 mr-1 object-contain dark:invert\"\n                                :src=\"SystemIcons.searchMatch\"\n                            />\n                            {{ $t(\"action.matchAction\") }}\n                        </div>\n                    </a-radio>\n                    <a-radio value=\"mcp\">\n                        <div class=\"flex items-center\">\n                            <img\n                                class=\"w-6 h-6 mr-1 object-contain dark:invert\"\n                                :src=\"SystemIcons.mcp\"\n                            />\n                            MCP\n                        </div>\n                    </a-radio>\n                </a-radio-group>\n            </div>\n            <div v-if=\"actionTab === 'keyword'\">\n                <m-empty v-if=\"!currentPluginActionsKeywordList.length\" />\n                <div v-for=\"a in currentPluginActionsKeywordList\" class=\"py-2\">\n                    <div class=\"mb-4 flex items-center\">\n                        <img\n                            class=\"w-6 h-6 object-contain mr-2\"\n                            :class=\"\n                                recordCurrent?.type === PluginType.SYSTEM\n                                    ? 'dark:invert'\n                                    : 'plugin-logo-filter'\n                            \"\n                            :src=\"a.icon\"\n                        />\n                        <div class=\"mr-2\">{{ a.title }}</div>\n                        <ActionTypeIcon class=\"mr-2\" :type=\"a.type\" />\n                        <a-tooltip\n                            :content=\"\n                                a['_pin']\n                                    ? $t('action.pinToSearch')\n                                    : $t('action.unpinFromSearch')\n                            \"\n                            position=\"left\"\n                        >\n                            <a\n                                href=\"javascript:;\"\n                                class=\"inline-block w-6 h-6 mr-2 bg-gray-100 dark:bg-gray-600 text-center leading-6 rounded-full\"\n                                :class=\"\n                                    a['_pin']\n                                        ? 'bg-gray-600 dark:bg-gray-200 text-white dark:text-black'\n                                        : ''\n                                \"\n                                @click=\"doPin(a as any)\"\n                            >\n                                <IconPin />\n                            </a>\n                        </a-tooltip>\n                    </div>\n                    <div\n                        v-for=\"m in a.matches.filter((m) =>\n                            ['text', 'key'].includes(\n                                (m as ActionMatchBase).type,\n                            ),\n                        )\"\n                        class=\"mr-1 mb-1 inline-block\"\n                    >\n                        <a-dropdown>\n                            <a-button-group>\n                                <a-button\n                                    v-if=\"\n                                        (m as ActionMatchBase).type === 'text'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ (m as ActionMatchText).text }}\n                                </a-button>\n                                <a-button\n                                    v-else-if=\"\n                                        (m as ActionMatchBase).type === 'key'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ (m as ActionMatchKey).key }}\n                                </a-button>\n                                <a-button\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    size=\"small\"\n                                >\n                                    <template #icon>\n                                        <icon-down />\n                                    </template>\n                                </a-button>\n                            </a-button-group>\n                            <template #content>\n                                <a-doption @click=\"doOpen(a as any)\">\n                                    {{ $t(\"common.open\") }}</a-doption\n                                >\n                                <a-doption\n                                    v-if=\"m['_disable']\"\n                                    @click=\"\n                                        doDisable(a as any, m.name as string)\n                                    \"\n                                >\n                                    {{ $t(\"common.enable\") }}\n                                </a-doption>\n                                <a-doption\n                                    v-else\n                                    @click=\"\n                                        doDisable(a as any, m.name as string)\n                                    \"\n                                >\n                                    {{ $t(\"common.disable\") }}\n                                </a-doption>\n                                <a-doption\n                                    @click=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                >\n                                    {{ $t(\"common.detail\") }}\n                                </a-doption>\n                            </template>\n                        </a-dropdown>\n                    </div>\n                </div>\n            </div>\n            <div v-else-if=\"actionTab === 'match'\">\n                <m-empty v-if=\"!currentPluginActionsMatchList.length\" />\n                <div v-for=\"a in currentPluginActionsMatchList\" class=\"py-2\">\n                    <div class=\"mb-4 flex items-center\">\n                        <img\n                            class=\"w-6 h-6 object-contain mr-2\"\n                            :class=\"\n                                recordCurrent?.type === PluginType.SYSTEM\n                                    ? 'dark:invert'\n                                    : 'plugin-logo-filter'\n                            \"\n                            :src=\"a.icon\"\n                        />\n                        <div class=\"mr-2\">{{ a.title }}</div>\n                        <ActionTypeIcon class=\"mr-2\" :type=\"a.type\" />\n                    </div>\n                    <div\n                        v-for=\"m in a.matches.filter((m) =>\n                            [\n                                'regex',\n                                'image',\n                                'file',\n                                'window',\n                                'editor',\n                            ].includes((m as ActionMatchBase).type),\n                        )\"\n                        class=\"mr-1 mb-1 inline-block\"\n                    >\n                        <a-dropdown>\n                            <a-button-group>\n                                <a-button\n                                    v-if=\"\n                                        (m as ActionMatchBase).type === 'regex'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ $t(\"action.regex\") }}\n                                    <div\n                                        class=\"inline-block max-w-32 overflow-hidden truncate\"\n                                    >\n                                        {{ (m as ActionMatchRegex).regex }}\n                                    </div>\n                                </a-button>\n                                <a-button\n                                    v-else-if=\"\n                                        (m as ActionMatchBase).type === 'image'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ $t(\"common.image\") }}\n                                </a-button>\n                                <a-button\n                                    v-else-if=\"\n                                        (m as ActionMatchBase).type === 'file'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ $t(\"common.file\") }}\n                                </a-button>\n                                <a-button\n                                    v-else-if=\"\n                                        (m as ActionMatchBase).type === 'window'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ $t(\"action.window\") }}\n                                    <div\n                                        class=\"inline-block max-w-32 overflow-hidden truncate\"\n                                    >\n                                        {{ (m as ActionMatchWindow).nameRegex }}\n                                        {{\n                                            (m as ActionMatchWindow).titleRegex\n                                        }}\n                                        {{ (m as ActionMatchWindow).attrRegex }}\n                                    </div>\n                                </a-button>\n                                <a-button\n                                    v-else-if=\"\n                                        (m as ActionMatchBase).type === 'editor'\n                                    \"\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    @click.stop=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                    size=\"small\"\n                                >\n                                    {{ $t(\"common.openFile\") }}\n                                    <div\n                                        class=\"inline-block max-w-32 overflow-hidden truncate\"\n                                    >\n                                        <span\n                                            v-if=\"\n                                                (m as ActionMatchEditor)\n                                                    .fadTypes\n                                            \"\n                                        >\n                                            {{\n                                                (\n                                                    m as ActionMatchEditor\n                                                ).fadTypes?.join(\",\")\n                                            }}\n                                        </span>\n                                        <span\n                                            v-if=\"\n                                                (m as ActionMatchEditor)\n                                                    .extensions\n                                            \"\n                                        >\n                                            {{\n                                                (\n                                                    m as ActionMatchEditor\n                                                ).extensions.join(\",\")\n                                            }}\n                                        </span>\n                                    </div>\n                                </a-button>\n                                <a-button\n                                    :type=\"\n                                        m['_disable'] ? undefined : 'primary'\n                                    \"\n                                    size=\"small\"\n                                >\n                                    <template #icon>\n                                        <icon-down />\n                                    </template>\n                                </a-button>\n                            </a-button-group>\n                            <template #content>\n                                <a-doption\n                                    v-if=\"m['_disable']\"\n                                    @click=\"\n                                        doDisable(a as any, m.name as string)\n                                    \"\n                                >\n                                    {{ $t(\"common.enable\") }}\n                                </a-doption>\n                                <a-doption\n                                    v-else\n                                    @click=\"\n                                        doDisable(a as any, m.name as string)\n                                    \"\n                                >\n                                    {{ $t(\"common.disable\") }}\n                                </a-doption>\n                                <a-doption\n                                    @click=\"\n                                        actionMatchDetailDialog?.show(\n                                            a as any,\n                                            m as any,\n                                        )\n                                    \"\n                                >\n                                    {{ $t(\"common.detail\") }}\n                                </a-doption>\n                            </template>\n                        </a-dropdown>\n                    </div>\n                </div>\n            </div>\n            <div v-else-if=\"actionTab === 'mcp'\">\n                <m-empty v-if=\"currentPluginMcpList.length === 0\" />\n                <div\n                    v-for=\"tool in currentPluginMcpList\"\n                    :key=\"tool.name\"\n                    class=\"mb-2 p-2 border border-gray-300 rounded-lg hover:bg-gray-100\"\n                >\n                    <div class=\"font-bold\">{{ tool.name }}</div>\n                    <div class=\"text-sm text-gray-600\">\n                        {{ tool.description }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n    <SystemActionMatchDetailDialog\n        ref=\"actionMatchDetailDialog\"\n        @disable=\"doDisable\"\n    />\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemSetting.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport { useManagerStore } from \"../../store/modules/manager\";\nimport { useSettingStore } from \"../../store/modules/setting\";\nimport { changeLocale, i18n, listLocales } from \"../../lang\";\nimport HotkeyInput from \"./components/HotkeyInput.vue\";\n\nconst setting = useSettingStore();\nconst manager = useManagerStore();\n\nconst isMacOs = focusany.isMacOs();\nconst isWindows = focusany.isWindows();\nconst isLinux = focusany.isLinux();\nconst fastPanelTriggerType = ref(\"Ctrl\");\nconst autoLaunchEnable = ref(false);\n\nonMounted(() => {\n    fastPanelTriggerType.value =\n        manager.configGet(\"fastPanelTrigger\", null).value?.type || \"Ctrl\";\n    autoLaunchEnable.value = setting.configGet(\"autoLaunchEnable\", false).value;\n});\n\nconst onManagerConfigChange = async (key: string, value: any) => {\n    // console.log('onManagerConfigChange', key, value)\n    switch (key) {\n        case \"fastPanelTriggerType\":\n            let valueArr = value.split(\":\");\n            let valueObj = {\n                type: valueArr[0],\n                times: valueArr.length > 1 ? parseInt(valueArr[1]) : 1,\n            };\n            await manager.onConfigChange(\"fastPanelTrigger\", valueObj);\n            fastPanelTriggerType.value = value;\n            break;\n        case \"autoLaunchEnable\":\n            await setting.onConfigChange(\"autoLaunchEnable\", value);\n            autoLaunchEnable.value = value;\n            await window.$mapi.app.setAutoLaunch(autoLaunchEnable.value);\n            autoLaunchEnable.value = await window.$mapi.app.getAutoLaunch();\n            break;\n    }\n};\n</script>\n\n<template>\n    <div class=\"p-4\">\n        <div class=\"mb-8\">\n            <div class=\"text-base font-bold mb-4\">\n                {{ $t(\"setting.functionSettings\") }}\n            </div>\n            <div class=\"pl-4\">\n                <div class=\"flex items-center mb-6\">\n                    <div class=\"flex-grow\">{{ $t(\"setting.language\") }}</div>\n                    <div>\n                        <a-select\n                            :model-value=\"i18n.global.locale.value\"\n                            @change=\"changeLocale($event as string)\"\n                        >\n                            <a-option\n                                v-for=\"item in listLocales()\"\n                                :key=\"item.name\"\n                                :value=\"item.name\"\n                                >{{ item.label }}</a-option\n                            >\n                        </a-select>\n                    </div>\n                </div>\n                <div class=\"flex items-center mb-6\">\n                    <div class=\"flex-grow\">\n                        {{ $t(\"setting.invokeHotkey\") }}\n                    </div>\n                    <div>\n                        <HotkeyInput\n                            :value=\"manager.configGet('mainTrigger', null)\"\n                            @change=\"\n                                manager.onConfigChange('mainTrigger', $event)\n                            \"\n                        />\n                    </div>\n                </div>\n                <div class=\"flex items-center mb-6\">\n                    <div class=\"flex-grow\">{{ $t(\"setting.themeStyle\") }}</div>\n                    <div>\n                        <a-radio-group\n                            :model-value=\"\n                                setting.configGet('darkMode', 'auto').value\n                            \"\n                            @change=\"setting.onConfigChange('darkMode', $event)\"\n                        >\n                            <a-radio value=\"light\">{{\n                                $t(\"setting.lightTheme\")\n                            }}</a-radio>\n                            <a-radio value=\"dark\">{{\n                                $t(\"setting.darkTheme\")\n                            }}</a-radio>\n                            <a-radio value=\"auto\">{{\n                                $t(\"setting.followSystem\")\n                            }}</a-radio>\n                        </a-radio-group>\n                    </div>\n                </div>\n                <div class=\"flex items-center mb-6\">\n                    <div class=\"flex-grow\">{{ $t(\"setting.fastPanel\") }}</div>\n                    <div>\n                        <a-switch\n                            :model-value=\"\n                                setting.configGet('fastPanelEnable', true).value\n                            \"\n                            @change=\"\n                                setting.onConfigChange(\n                                    'fastPanelEnable',\n                                    $event,\n                                )\n                            \"\n                        />\n                    </div>\n                </div>\n                <div\n                    class=\"flex items-center mb-6\"\n                    v-if=\"setting.configGet('fastPanelEnable', true).value\"\n                >\n                    <div class=\"flex-grow\">\n                        {{ $t(\"setting.fastPanelHotkey\") }}\n                    </div>\n                    <div>\n                        <a-select\n                            :model-value=\"fastPanelTriggerType\"\n                            @change=\"\n                                onManagerConfigChange(\n                                    'fastPanelTriggerType',\n                                    $event,\n                                )\n                            \"\n                        >\n                            <a-option\n                                value=\"Ctrl\"\n                                v-if=\"isWindows || isLinux\"\n                                >{{ $t(\"setting.ctrlSingleClick\") }}</a-option\n                            >\n                            <a-option value=\"Ctrl\" v-else-if=\"isMacOs\">{{\n                                $t(\"setting.controlSingleClick\")\n                            }}</a-option>\n                            <a-option\n                                value=\"Ctrl:2\"\n                                v-if=\"isWindows || isLinux\"\n                                >{{ $t(\"setting.ctrlDoubleClick\") }}</a-option\n                            >\n                            <a-option value=\"Ctrl:2\" v-else-if=\"isMacOs\">{{\n                                $t(\"setting.controlDoubleClick\")\n                            }}</a-option>\n                            <a-option value=\"Alt\" v-if=\"isWindows || isLinux\">{{\n                                $t(\"setting.altSingleClick\")\n                            }}</a-option>\n                            <a-option value=\"Alt\" v-else-if=\"isMacOs\">{{\n                                $t(\"setting.optionSingleClick\")\n                            }}</a-option>\n                            <a-option\n                                value=\"Alt:2\"\n                                v-if=\"isWindows || isLinux\"\n                                >{{ $t(\"setting.altDoubleClick\") }}</a-option\n                            >\n                            <a-option value=\"Alt:2\" v-else-if=\"isMacOs\">{{\n                                $t(\"setting.optionDoubleClick\")\n                            }}</a-option>\n                            <a-option value=\"Meta\" v-if=\"isMacOs\">{{\n                                $t(\"setting.commandSingleClick\")\n                            }}</a-option>\n                            <a-option value=\"Meta:2\" v-if=\"isMacOs\">{{\n                                $t(\"setting.commandDoubleClick\")\n                            }}</a-option>\n                        </a-select>\n                    </div>\n                </div>\n                <div class=\"flex items-center mb-6\">\n                    <div class=\"flex-grow\">{{ $t(\"setting.autoLaunch\") }}</div>\n                    <div>\n                        <a-switch\n                            :model-value=\"autoLaunchEnable\"\n                            @change=\"\n                                onManagerConfigChange(\n                                    'autoLaunchEnable',\n                                    $event,\n                                )\n                            \"\n                        />\n                    </div>\n                </div>\n                <div class=\"flex items-center mb-6\">\n                    <div class=\"flex-grow\">\n                        {{ $t(\"setting.detachWindowHotkey\") }}\n                    </div>\n                    <div>\n                        <HotkeyInput\n                            :value=\"\n                                manager.configGet('detachWindowTrigger', null)\n                            \"\n                            @change=\"\n                                manager.onConfigChange(\n                                    'detachWindowTrigger',\n                                    $event,\n                                )\n                            \"\n                        />\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/System/SystemUser.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport PageWebviewStatus from \"../../components/common/PageWebviewStatus.vue\";\nimport { useUserPage } from \"../../hooks/user\";\n\nconst status = ref<InstanceType<typeof PageWebviewStatus> | null>(null);\nconst web = ref<any | null>(null);\n\nconst { webPreload, webUrl, webUserAgent, user, canGoBack, doBack, onMount } =\n    useUserPage({ web, status });\n\nonMounted(async () => {\n    await onMount();\n});\n</script>\n\n<template>\n    <div class=\"pb-user-container relative\">\n        <div>\n            <webview\n                ref=\"web\"\n                :src=\"webUrl\"\n                nodeintegration\n                :useragent=\"webUserAgent\"\n                :preload=\"webPreload\"\n                class=\"pb-user-web\"\n            ></webview>\n            <div class=\"absolute left-5 top-5 z-40\">\n                <a-button\n                    v-if=\"canGoBack\"\n                    @click=\"doBack\"\n                    type=\"secondary\"\n                    shape=\"round\"\n                >\n                    <template #icon>\n                        <icon-left />\n                    </template>\n                    {{ $t(\"common.back\") }}\n                </a-button>\n            </div>\n        </div>\n        <PageWebviewStatus ref=\"status\" />\n    </div>\n</template>\n\n<style lang=\"less\" scoped>\n.pb-user-container,\n.pb-user-web {\n    width: 100%;\n    height: calc(100vh - 1px);\n}\n</style>\n"
  },
  {
    "path": "src/pages/System/components/ActionTypeIcon.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport IconCodeBraces from \"~icons/mdi/code-braces\";\nimport IconCodeTags from \"~icons/mdi/code-tags\";\nimport IconConsole from \"~icons/mdi/console\";\nimport IconEyeOutline from \"~icons/mdi/eye-outline\";\nimport { t } from \"../../../lang\";\nimport { ActionTypeEnum } from \"../../../types/Manager\";\n\nconst props = defineProps<{\n    type: ActionTypeEnum | undefined;\n}>();\n\nconst typeTitle = computed(() => {\n    switch (props.type) {\n        case ActionTypeEnum.WEB:\n            return t(\"action.webpage\");\n        case ActionTypeEnum.COMMAND:\n            return t(\"action.command\");\n        case ActionTypeEnum.VIEW:\n            return t(\"action.smartArea\");\n        case ActionTypeEnum.CODE:\n            return t(\"action.code\");\n        case ActionTypeEnum.BACKEND:\n            return t(\"action.backendCode\");\n        default:\n            return \"\";\n    }\n});\n\nconst iconMap: Record<string, any> = {\n    [ActionTypeEnum.BACKEND]: IconCodeBraces,\n    [ActionTypeEnum.COMMAND]: IconConsole,\n    [ActionTypeEnum.VIEW]: IconEyeOutline,\n    [ActionTypeEnum.CODE]: IconCodeTags,\n};\n\nconst currentIcon = computed(() =>\n    props.type ? (iconMap[props.type] ?? null) : null,\n);\n</script>\n\n<template>\n    <div v-if=\"type !== ActionTypeEnum.WEB && currentIcon\">\n        <a-tooltip :content=\"typeTitle\">\n            <component :is=\"currentIcon\" />\n        </a-tooltip>\n    </div>\n</template>\n"
  },
  {
    "path": "src/pages/System/components/HotkeyInput.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, onBeforeMount, ref, watch } from \"vue\";\nimport { HotkeyKeyItem } from \"../../../../electron/mapi/keys/type\";\nimport { t } from \"../../../lang\";\n\nconst focus = ref(false);\nconst platformName = ref<\"win\" | \"osx\" | \"linux\" | null>(null);\n\nonBeforeMount(() => {\n    platformName.value = window.$mapi?.app?.platformName() as any;\n});\n\nconst props = defineProps({\n    value: {\n        type: Object,\n        default: null,\n    },\n});\nconst currentValue = ref<HotkeyKeyItem | null>(null);\nlet newValue = null as HotkeyKeyItem | null;\n\nconst emit = defineEmits([\"change\"]);\n\nwatch(\n    () => props.value,\n    (value) => {\n        if (value) {\n            if (value.value) {\n                currentValue.value = value.value;\n            } else {\n                currentValue.value = value as HotkeyKeyItem;\n            }\n        } else {\n            currentValue.value = null;\n        }\n    },\n    {\n        immediate: true,\n    },\n);\n\nlet lastKeyTime = 0;\nlet lastKey = \"\";\nconst showHotkey = computed(() => {\n    newValue = null;\n    if (!currentValue.value) {\n        return null;\n    }\n    // console.log('currentValue.value', JSON.stringify(currentValue.value, null, 2))\n    const { key, altKey, ctrlKey, metaKey, shiftKey } =\n        currentValue.value as HotkeyKeyItem;\n    const texts: string[] = [];\n    if (ctrlKey) {\n        if (platformName.value === \"osx\") {\n            texts.push(\"Control\");\n        } else {\n            texts.push(\"Ctrl\");\n        }\n    }\n    if (altKey) {\n        if (platformName.value === \"osx\") {\n            texts.push(\"Option\");\n        } else {\n            texts.push(\"Alt\");\n        }\n    }\n    if (metaKey) {\n        if (platformName.value === \"osx\") {\n            texts.push(\"Command\");\n        } else if (platformName.value === \"win\") {\n            texts.push(\"Win\");\n        } else {\n            texts.push(\"Meta\");\n        }\n    }\n    if (shiftKey) {\n        texts.push(\"Shift\");\n    }\n    if (key) {\n        const valid = [\n            \"A\",\n            \"B\",\n            \"C\",\n            \"D\",\n            \"E\",\n            \"F\",\n            \"G\",\n            \"H\",\n            \"I\",\n            \"J\",\n            \"K\",\n            \"L\",\n            \"M\",\n            \"N\",\n            \"O\",\n            \"P\",\n            \"Q\",\n            \"R\",\n            \"S\",\n            \"T\",\n            \"U\",\n            \"V\",\n            \"W\",\n            \"X\",\n            \"Y\",\n            \"Z\",\n            \"1\",\n            \"2\",\n            \"3\",\n            \"4\",\n            \"5\",\n            \"6\",\n            \"7\",\n            \"8\",\n            \"9\",\n            \"0\",\n            \"Space\",\n        ];\n        if (valid.includes(key)) {\n            texts.push(key);\n        }\n    }\n    if (lastKeyTime > 0 && lastKeyTime < Date.now() - 500) {\n        lastKeyTime = 0;\n    }\n    let times = 1;\n    if (lastKey === texts.join(\"+\") && lastKeyTime > 0) {\n        times = 2;\n    }\n    lastKey = texts.join(\"+\");\n    lastKeyTime = Date.now();\n    if (times > 1) {\n        lastKey = \"\";\n    }\n    newValue = {\n        key: key,\n        altKey: altKey,\n        ctrlKey: ctrlKey,\n        metaKey: metaKey,\n        shiftKey: shiftKey,\n        times: times,\n    } as HotkeyKeyItem;\n    return texts.join(\"+\") + (times > 1 ? t(\"hotkey.doubleClick\") : \"\");\n});\n\nconst onHotkey = (data: any) => {\n    currentValue.value = {\n        key: data.key,\n        altKey: data.altKey,\n        ctrlKey: data.ctrlKey,\n        metaKey: data.metaKey,\n        shiftKey: data.shiftKey,\n        times: data.times,\n    };\n};\n\nconst onFocus = async () => {\n    focus.value = true;\n    await window.$mapi.manager.hotKeyWatch();\n    window.__page.onBroadcast(\"HotkeyWatch\", onHotkey);\n};\nconst onBlur = () => {\n    focus.value = false;\n    window.__page.offBroadcast(\"HotkeyWatch\", onHotkey);\n    window.$mapi.manager.hotKeyUnwatch();\n    if (newValue) {\n        // console.log('newValue', JSON.stringify(newValue))\n        emit(\"change\", newValue);\n    }\n};\nconst content = computed(() => {\n    return [\n        t(\"hotkey.instructions\"),\n        t(\"hotkey.step1\"),\n        platformName.value === \"osx\"\n            ? t(\"hotkey.step2Mac\")\n            : t(\"hotkey.step2Win\"),\n    ].join(\"\");\n});\n</script>\n\n<template>\n    <a-tooltip :content=\"content\">\n        <input\n            class=\"border-2 border-solid border-gray-300 dark:border-gray-600 dark:bg-gray-700 h-9 w-48 text-center rounded-lg cursor-pointer flex outline-none select-none\"\n            @focus=\"onFocus\"\n            @blur=\"onBlur\"\n            :value=\"showHotkey ? showHotkey : $t('hotkey.notSet')\"\n            readonly\n            :class=\"{ active: focus }\"\n        />\n    </a-tooltip>\n</template>\n\n<style scoped lang=\"less\">\n.active {\n    border-color: var(--color-primary) !important;\n}\n</style>\n"
  },
  {
    "path": "src/pages/System/components/SystemActionMatchDetailDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { SystemIcons } from \"../../../../electron/mapi/manager/system/asset/icon\";\nimport {\n    ActionMatch,\n    ActionMatchFile,\n    ActionMatchKey,\n    ActionMatchRegex,\n    ActionMatchText,\n    ActionMatchTypeEnum,\n    ActionRecord,\n} from \"../../../types/Manager\";\n\nconst visible = ref(false);\nconst action = ref<ActionRecord | null>(null);\nconst match = ref<ActionMatch | null>(null);\n\nconst show = async (a: ActionRecord, m: ActionMatch) => {\n    action.value = a;\n    match.value = m;\n    visible.value = true;\n};\n\nconst emit = defineEmits([\"disable\"]);\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal v-model:visible=\"visible\" title-align=\"start\">\n        <template #title>\n            <div\n                v-if=\"['text', 'key'].includes(match?.type as string)\"\n                class=\"flex items-center\"\n            >\n                <img\n                    class=\"w-6 h-6 object-contain mr-2\"\n                    :src=\"SystemIcons.searchKeyword\"\n                />\n                {{ $t(\"action.searchAction\") }}\n            </div>\n            <div v-else class=\"flex items-center\">\n                <img\n                    class=\"w-6 h-6 object-contain mr-2\"\n                    :src=\"SystemIcons.searchMatch\"\n                />\n                {{ $t(\"action.matchAction\") }}\n            </div>\n        </template>\n        <template #footer>\n            <a-button\n                type=\"primary\"\n                size=\"small\"\n                status=\"danger\"\n                v-if=\"!match?.['_disable']\"\n                @click=\"emit('disable', action, match?.name)\"\n            >\n                {{ $t(\"common.disable\") }}\n            </a-button>\n            <a-button\n                type=\"primary\"\n                size=\"small\"\n                v-else\n                @click=\"emit('disable', action, match?.name)\"\n            >\n                {{ $t(\"common.enable\") }}\n            </a-button>\n            <a-button size=\"small\" @click=\"visible = false\">\n                {{ $t(\"common.close\") }}\n            </a-button>\n        </template>\n        <div class=\"h-64\">\n            <div v-if=\"match?.type === ActionMatchTypeEnum.TEXT\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchKeywordHint\") }}\n                </div>\n                <div\n                    class=\"text-center text-lg bg-gray-100 dark:bg-gray-700 rounded-lg p-3 font-weight\"\n                >\n                    {{ (match as ActionMatchText).text }}\n                </div>\n            </div>\n            <div v-else-if=\"match?.type === ActionMatchTypeEnum.KEY\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchExactKeyHint\") }}\n                </div>\n                <div\n                    class=\"text-center text-lg bg-gray-100 dark:bg-gray-700 rounded-lg p-3 font-weight\"\n                >\n                    {{ (match as ActionMatchKey).key }}\n                </div>\n            </div>\n            <div v-else-if=\"match?.type === ActionMatchTypeEnum.REGEX\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchRegexHint\") }}\n                </div>\n                <div\n                    class=\"text-center text-lg bg-gray-100 dark:bg-gray-700 rounded-lg p-3 font-weight\"\n                >\n                    {{ (match as ActionMatchRegex).regex }}\n                </div>\n            </div>\n            <div v-else-if=\"match?.type === ActionMatchTypeEnum.IMAGE\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchImageHint\") }}\n                </div>\n            </div>\n            <div v-else-if=\"match?.type === ActionMatchTypeEnum.FILE\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchFileHint\") }}\n                </div>\n                <div class=\"bg-gray-100 dark:bg-gray-700 rounded-lg p-3\">\n                    <div v-if=\"'minCount' in match\">\n                        {{ $t(\"action.minCount\") }}:\n                        {{ (match as ActionMatchFile).minCount }}\n                    </div>\n                    <div v-if=\"'maxCount' in match\">\n                        {{ $t(\"action.maxCount\") }}:\n                        {{ (match as ActionMatchFile).maxCount }}\n                    </div>\n                    <div v-if=\"'filterExtensions' in match\">\n                        {{ $t(\"action.fileExtensions\") }}:\n                        {{\n                            (match as ActionMatchFile).filterExtensions.join(\n                                \",\",\n                            )\n                        }}\n                    </div>\n                    <div v-if=\"'filterFileType' in match\">\n                        {{ $t(\"action.fileType\") }}:\n                        {{\n                            (match as ActionMatchFile).filterFileType === \"file\"\n                                ? $t(\"common.file\")\n                                : $t(\"common.folder\")\n                        }}\n                    </div>\n                </div>\n            </div>\n            <div v-else-if=\"match?.type === ActionMatchTypeEnum.WINDOW\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchWindowHint\") }}\n                </div>\n                <div class=\"bg-gray-100 dark:bg-gray-700 rounded-lg p-3\">\n                    <div v-if=\"'nameRegex' in match\">\n                        {{ $t(\"action.nameMatch\") }}：{{\n                            (match as ActionMatchWindow).nameRegex\n                        }}\n                    </div>\n                    <div v-if=\"'titleRegex' in match\">\n                        {{ $t(\"action.titleMatch\") }}:\n                        {{ (match as ActionMatchWindow).titleRegex }}\n                    </div>\n                    <div v-if=\"'attrRegex' in match\">\n                        {{ $t(\"action.attrMatch\") }}:\n                        {{ (match as ActionMatchWindow).attrRegex }}\n                    </div>\n                </div>\n            </div>\n            <div v-else-if=\"match?.type === ActionMatchTypeEnum.EDITOR\">\n                <div class=\"mb-3\">\n                    <icon-info-circle />\n                    {{ $t(\"action.matchEditorHint\") }}\n                </div>\n                <div class=\"bg-gray-100 dark:bg-gray-700 rounded-lg p-3\">\n                    <div v-if=\"'extensions' in match\">\n                        {{ $t(\"action.suffix\") }}：{{\n                            (match as ActionMatchEditor).extensions.join(\",\")\n                        }}\n                    </div>\n                    <div v-if=\"'fadTypes' in match\">\n                        {{ $t(\"common.type\") }}：{{\n                            (match as ActionMatchEditor).fadTypes.join(\",\")\n                        }}\n                    </div>\n                </div>\n            </div>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/pages/System/components/SystemDataBackup/WebDavManage.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, ref, watch } from \"vue\";\nimport { t } from \"../../../../lang\";\nimport { Dialog } from \"../../../../lib/dialog\";\nimport { TimeUtil } from \"../../../../lib/util\";\nimport WebDavManageSettingDialog from \"./WebDavManageSettingDialog.vue\";\n\nconst type = ref(\"backup\");\nconst settingDialog = ref<InstanceType<\n    typeof WebDavManageSettingDialog\n> | null>(null);\nconst backupWebDavHasConfig = ref(false);\nconst loading = ref(false);\nconst restoreRecords = ref<string[]>([]);\nconst restoreRecordSelect = ref<string | null>(null);\n\nonMounted(() => {\n    doLoad();\n});\n\nwatch(\n    () => type.value,\n    () => {\n        if (type.value === \"restore\") {\n            doLoadRestoreRecords();\n        }\n    },\n);\n\nconst doLoad = async () => {\n    const backupWebdav = await window.$mapi.config.get(\"backupWebdav\", {});\n    backupWebDavHasConfig.value = !!backupWebdav[\"url\"];\n};\nconst doLoadRestoreRecords = async () => {\n    const backupWebdav = await window.$mapi.config.get(\"backupWebdav\", {});\n    let root = backupWebdav.root || \"/FocusAnyBackup/\";\n    const records = await window.$mapi.kvdb.listWebDav(root, backupWebdav);\n    if (records.length > 0) {\n        restoreRecordSelect.value = records[0];\n    }\n    restoreRecords.value = records;\n};\n\nconst doBackup = async () => {\n    if (loading.value) {\n        return;\n    }\n    const backupWebdav = await window.$mapi.config.get(\"backupWebdav\", {});\n    let file =\n        backupWebdav.filePattern ||\n        \"Backup-{year}{month}{day}{hour}{minute}{second}\";\n    let root = backupWebdav.root || \"/FocusAnyBackup/\";\n    file = TimeUtil.replacePattern(file);\n    root = root.replace(/\\/$/, \"\");\n    file = `${root}/${file}.backup`;\n    Dialog.loadingOn(t(\"backup.backingUp\"));\n    loading.value = true;\n    try {\n        await window.$mapi.kvdb.dumpToWebDav(file, backupWebdav);\n        Dialog.tipSuccess(t(\"backup.backupSuccess\"));\n    } catch (e) {\n        Dialog.tipError(t(\"backup.backupFailed\"));\n    } finally {\n        loading.value = false;\n        Dialog.loadingOff();\n    }\n};\n\nconst doRestore = async () => {\n    if (loading.value) {\n        return;\n    }\n    if (!restoreRecordSelect.value) {\n        Dialog.tipError(t(\"backup.selectRestoreFile\"));\n        return;\n    }\n    const backupWebdav = await window.$mapi.config.get(\"backupWebdav\", {});\n    let root = backupWebdav.root || \"/FocusAnyBackup/\";\n    root = root.replace(/\\/$/, \"\") + \"/\";\n    let file = restoreRecordSelect.value;\n    file = root + file;\n    Dialog.loadingOn(t(\"backup.restoring\"));\n    loading.value = true;\n    try {\n        await window.$mapi.kvdb.importFromWebDav(file, backupWebdav);\n        Dialog.tipSuccess(t(\"backup.restoreSuccess\"));\n    } catch (e) {\n        Dialog.tipError(t(\"backup.restoreFailed\"));\n    } finally {\n        loading.value = false;\n        Dialog.loadingOff();\n    }\n};\n\nconst emit = defineEmits([\"update\"]);\n</script>\n\n<template>\n    <div class=\"flex\">\n        <div class=\"flex-grow\">\n            <a-radio-group v-model:model-value=\"type\">\n                <a-radio value=\"backup\">{{\n                    $t(\"backup.uploadToCloud\")\n                }}</a-radio>\n                <a-radio value=\"restore\">{{\n                    $t(\"backup.restoreFromCloud\")\n                }}</a-radio>\n            </a-radio-group>\n        </div>\n        <div>\n            <a-button\n                v-if=\"backupWebDavHasConfig\"\n                size=\"small\"\n                @click=\"settingDialog?.show()\"\n            >\n                <template #icon>\n                    <icon-settings />\n                </template>\n                {{ $t(\"backup.webdavConfig\") }}\n            </a-button>\n        </div>\n    </div>\n    <div class=\"py-3\" v-if=\"!backupWebDavHasConfig\">\n        <div\n            class=\"bg-gray-100 dark:bg-gray-700 rounded-lg text-center p-4 cursor-pointer\"\n            @click=\"settingDialog?.show()\"\n        >\n            <div>\n                <icon-cloud class=\"text-2xl\" />\n            </div>\n            <div>{{ $t(\"backup.notConfigured\") }}</div>\n        </div>\n    </div>\n    <div class=\"py-3\" v-if=\"backupWebDavHasConfig && type === 'backup'\">\n        <div\n            class=\"bg-gray-100 dark:bg-gray-700 rounded-lg text-center p-4 hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer\"\n            @click=\"doBackup\"\n        >\n            <div>\n                <icon-cloud class=\"text-2xl\" />\n            </div>\n            <div>{{ $t(\"backup.startBackup\") }}</div>\n        </div>\n    </div>\n    <div class=\"py-3\" v-if=\"backupWebDavHasConfig && type === 'restore'\">\n        <a-form :model=\"{}\" layout=\"vertical\">\n            <a-form-item>\n                <a-select\n                    v-model=\"restoreRecordSelect as any\"\n                    style=\"width: 100%\"\n                >\n                    <a-option\n                        v-for=\"item in restoreRecords\"\n                        :key=\"item\"\n                        :value=\"item\"\n                    >\n                        {{ item }}\n                    </a-option>\n                </a-select>\n                <template #label>\n                    <div class=\"flex items-center\">\n                        <div class=\"mr-2\">{{ $t(\"backup.selectFile\") }}</div>\n                        <a-button size=\"small\" @click=\"doLoadRestoreRecords\">\n                            <template #icon>\n                                <icon-refresh />\n                            </template>\n                        </a-button>\n                    </div>\n                </template>\n            </a-form-item>\n            <a-form-item>\n                <a-button type=\"primary\" @click=\"doRestore\">\n                    {{ $t(\"backup.startRestore\") }}\n                </a-button>\n            </a-form-item>\n        </a-form>\n    </div>\n    <WebDavManageSettingDialog ref=\"settingDialog\" @update=\"doLoad()\" />\n</template>\n\n<style scoped lang=\"less\"></style>\n"
  },
  {
    "path": "src/pages/System/components/SystemDataBackup/WebDavManageSettingDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, toRaw } from \"vue\";\nimport { t } from \"../../../../lang\";\nimport { Dialog } from \"../../../../lib/dialog\";\n\nconst visible = ref(false);\nconst formData = ref({\n    url: \"\",\n    username: \"\",\n    password: \"\",\n    root: \"\",\n    filePattern: \"\",\n});\nconst show = async () => {\n    const backupWebdav = await window.$mapi.config.get(\"backupWebdav\", {});\n    formData.value.url = backupWebdav[\"url\"] || \"\";\n    formData.value.username = backupWebdav[\"username\"] || \"\";\n    formData.value.password = backupWebdav[\"password\"] || \"\";\n    formData.value.root = backupWebdav[\"root\"] || \"/FocusAnyBackup/\";\n    formData.value.filePattern =\n        backupWebdav[\"filePattern\"] ||\n        \"Backup-{year}{month}{day}{hour}{minute}{second}\";\n    visible.value = true;\n};\n\nconst doSubmit = async () => {\n    try {\n        await window.$mapi.kvdb.testWebdav(toRaw(formData.value));\n    } catch (e) {\n        // console.error('testWebdav', e)\n        Dialog.tipError(t(\"backup.connectFailed\"));\n        return;\n    }\n    await window.$mapi.config.set(\"backupWebdav\", {\n        url: formData.value.url,\n        username: formData.value.username,\n        password: formData.value.password,\n        root: formData.value.root,\n        filePattern: formData.value.filePattern,\n    });\n    visible.value = false;\n    emit(\"update\");\n};\n\nconst emit = defineEmits([\"update\"]);\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal v-model:visible=\"visible\" title-align=\"start\">\n        <template #title> {{ $t(\"backup.webdavSettings\") }} </template>\n        <template #footer>\n            <a-button type=\"primary\" size=\"small\" @click=\"doSubmit\">\n                {{ $t(\"common.save\") }}\n            </a-button>\n            <a-button size=\"small\" @click=\"visible = false\">\n                {{ $t(\"common.close\") }}\n            </a-button>\n        </template>\n        <div class=\"h-64\">\n            <a-form :model=\"{}\">\n                <a-form-item label=\"URL\">\n                    <a-input\n                        v-model:model-value=\"formData.url\"\n                        placeholder=\"https://\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('backup.username')\">\n                    <a-input v-model:model-value=\"formData.username\" />\n                </a-form-item>\n                <a-form-item :label=\"$t('backup.password')\">\n                    <a-input\n                        v-model:model-value=\"formData.password\"\n                        type=\"password\"\n                    />\n                </a-form-item>\n                <a-form-item :label=\"$t('backup.rootDir')\">\n                    <a-input v-model:model-value=\"formData.root\" />\n                </a-form-item>\n                <a-form-item :label=\"$t('backup.fileFormat')\">\n                    <a-input v-model:model-value=\"formData.filePattern\" />\n                    <template #help>\n                        <div class=\"text-gray-400\">\n                            {{ $t(\"backup.placeholderSupport\") }} {year} {month}\n                            {day} {hour} {minute} {second}\n                        </div>\n                    </template>\n                </a-form-item>\n            </a-form>\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/pages/System/components/SystemDataBackupDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { t } from \"../../../lang\";\nimport { Dialog } from \"../../../lib/dialog\";\nimport { TimeUtil } from \"../../../lib/util\";\nimport WebDavManage from \"./SystemDataBackup/WebDavManage.vue\";\n\nconst visible = ref(false);\nconst loading = ref(false);\nconst type = ref(\"backup\");\n\nconst open = async () => {\n    visible.value = true;\n};\n\nconst onClose = () => {\n    visible.value = false;\n};\n\nconst doBackup = async () => {\n    if (loading.value) {\n        return;\n    }\n    const file = await window.$mapi.file.openSave({\n        defaultPath: `FocusAny-Backup-${TimeUtil.datetimeString()}.backup`,\n    });\n    if (!file) {\n        return;\n    }\n    Dialog.loadingOn(t(\"backup.backingUp\"));\n    loading.value = true;\n    try {\n        await window.$mapi.kvdb.dumpToFile(file);\n        Dialog.tipSuccess(t(\"backup.backupSuccess\"));\n    } catch (e) {\n        Dialog.tipError(t(\"backup.backupFailed\"));\n    } finally {\n        loading.value = false;\n        Dialog.loadingOff();\n    }\n};\n\nconst doRestore = async () => {\n    if (loading.value) {\n        return;\n    }\n    const file = await window.$mapi.file.openFile({\n        filters: [{ name: \"Backup\", extensions: [\"backup\"] }],\n    });\n    if (!file) {\n        return;\n    }\n    Dialog.loadingOn(t(\"backup.restoring\"));\n    loading.value = true;\n    try {\n        await window.$mapi.kvdb.importFromFile(file);\n        loading.value = false;\n        Dialog.tipSuccess(t(\"backup.restoreSuccess\"));\n        await window.$mapi.manager.clearCache();\n        await onUpdate();\n    } catch (e) {\n        Dialog.tipError(t(\"backup.restoreFailed\"));\n    } finally {\n        loading.value = false;\n        Dialog.loadingOff();\n    }\n};\n\nconst onUpdate = async () => {\n    emit(\"update\");\n};\n\nconst emit = defineEmits([\"update\"]);\n\ndefineExpose({\n    open,\n});\n</script>\n\n<template>\n    <a-drawer\n        :width=\"500\"\n        :visible=\"visible\"\n        @close=\"onClose\"\n        @ok=\"onClose\"\n        @cancel=\"onClose\"\n        unmountOnClose\n    >\n        <template #title> {{ $t(\"backup.title\") }} </template>\n        <template #footer>\n            <a-button size=\"small\" @click=\"onClose()\">\n                {{ $t(\"common.close\") }}\n            </a-button>\n        </template>\n        <div style=\"margin: -12px -16px\">\n            <div class=\"p-3\">\n                <a-radio-group type=\"button\" v-model:model-value=\"type\">\n                    <a-radio value=\"backup\">{{\n                        $t(\"backup.backupToFile\")\n                    }}</a-radio>\n                    <a-radio value=\"restore\">{{\n                        $t(\"backup.restoreFromFile\")\n                    }}</a-radio>\n                    <a-radio value=\"webdav\">WebDav</a-radio>\n                </a-radio-group>\n            </div>\n            <div class=\"p-3\" v-if=\"type === 'backup'\">\n                <div\n                    class=\"bg-gray-100 dark:bg-gray-700 rounded-lg text-center p-4 hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer\"\n                    @click=\"doBackup\"\n                >\n                    <div>\n                        <icon-download class=\"text-2xl\" />\n                    </div>\n                    <div>{{ $t(\"backup.backupToLocal\") }}</div>\n                </div>\n                <div class=\"pt-3\">\n                    <a-alert> {{ $t(\"backup.formatTip\") }} </a-alert>\n                </div>\n            </div>\n            <div class=\"p-3\" v-if=\"type === 'restore'\">\n                <div\n                    class=\"bg-gray-100 dark:bg-gray-700 rounded-lg text-center p-4 hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer\"\n                    @click=\"doRestore\"\n                >\n                    <div>\n                        <icon-upload class=\"text-2xl\" />\n                    </div>\n                    <div>{{ $t(\"backup.restoreFromLocal\") }}</div>\n                </div>\n                <div class=\"pt-3\">\n                    <a-alert> {{ $t(\"backup.formatTip\") }} </a-alert>\n                </div>\n            </div>\n            <div class=\"p-3\" v-if=\"type === 'webdav'\">\n                <WebDavManage @update=\"emit('update')\" />\n            </div>\n        </div>\n    </a-drawer>\n</template>\n\n<style scoped lang=\"less\"></style>\n"
  },
  {
    "path": "src/pages/System/components/SystemDataViewDetailDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport CodeViewer from \"../../../components/common/CodeViewer.vue\";\nimport { t } from \"../../../lang\";\nimport { Dialog } from \"../../../lib/dialog\";\nimport { SystemDataRecord } from \"./type\";\n\nconst visible = ref(false);\nconst record = ref<SystemDataRecord | null>(null);\nconst recordDetail = ref<any | null>(null);\nconst key = ref(\"\");\n\nconst show = async (r: SystemDataRecord, k: string) => {\n    record.value = r;\n    key.value = k;\n    visible.value = true;\n    await doLoad();\n};\n\nconst doLoad = async () => {\n    recordDetail.value = await window.$mapi.kvdb.get(\n        record.value?.plugin.name as string,\n        key.value,\n    );\n};\n\nconst doDelete = async () => {\n    Dialog.confirm(t(\"data.deleteConfirm\")).then(async () => {\n        Dialog.loadingOn();\n        await window.$mapi.kvdb.remove(\n            record.value?.plugin.name as string,\n            key.value,\n        );\n        Dialog.loadingOff();\n        Dialog.tipSuccess(t(\"data.deleteSuccess\"));\n        visible.value = false;\n        emit(\"update\");\n    });\n};\n\nconst emit = defineEmits([\"update\"]);\n\ndefineExpose({\n    show,\n});\n</script>\n\n<template>\n    <a-modal v-model:visible=\"visible\" title-align=\"start\">\n        <template #title>\n            <div class=\"truncate hover:bg-gray-100 cursor-pointer w-96\">\n                {{ key }}\n            </div>\n        </template>\n        <template #footer>\n            <a-button\n                type=\"primary\"\n                size=\"small\"\n                status=\"danger\"\n                @click=\"doDelete\"\n            >\n                {{ $t(\"common.delete\") }}\n            </a-button>\n            <a-button size=\"small\" @click=\"visible = false\">\n                {{ $t(\"common.close\") }}\n            </a-button>\n        </template>\n        <div class=\"h-64\" style=\"margin: -24px -20px\">\n            <CodeViewer\n                lang=\"json\"\n                :code=\"JSON.stringify(recordDetail, null, 2)\"\n            />\n        </div>\n    </a-modal>\n</template>\n"
  },
  {
    "path": "src/pages/System/components/SystemDataViewDialog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport MEmpty from \"../../../components/common/MEmpty.vue\";\nimport { t } from \"../../../lang\";\nimport { Dialog } from \"../../../lib/dialog\";\nimport SystemDataViewDetailDialog from \"./SystemDataViewDetailDialog.vue\";\nimport { SystemDataRecord } from \"./type\";\n\nconst dataViewDetailDialog = ref<InstanceType<\n    typeof SystemDataViewDetailDialog\n> | null>();\nconst visible = ref(false);\nconst loading = ref(false);\nconst record = ref<SystemDataRecord | null>(null);\nconst keys = ref([] as string[]);\n\nconst open = async (r: SystemDataRecord) => {\n    record.value = r;\n    keys.value = [];\n    visible.value = true;\n    await doLoad();\n};\n\nconst onClose = () => {\n    visible.value = false;\n};\n\nconst doTruncate = async () => {\n    Dialog.confirm(t(\"data.clearConfirm\")).then(async () => {\n        Dialog.loadingOn();\n        for (const k of keys.value) {\n            await window.$mapi.kvdb.remove(\n                record.value?.plugin.name as string,\n                k,\n            );\n        }\n        keys.value = [];\n        await doLoad();\n        Dialog.loadingOff();\n        Dialog.tipSuccess(t(\"data.clearSuccess\"));\n        visible.value = false;\n        emit(\"update\");\n    });\n};\n\nconst doLoad = async () => {\n    loading.value = true;\n    keys.value = await window.$mapi.kvdb.allKeys(\n        record.value?.plugin.name as string,\n        \"\",\n    );\n    loading.value = false;\n};\n\nconst onUpdate = async () => {\n    await doLoad();\n    emit(\"update\");\n};\n\nconst emit = defineEmits([\"update\"]);\n\ndefineExpose({\n    open,\n});\n</script>\n\n<template>\n    <a-drawer\n        :width=\"340\"\n        class=\"pb-system-data-view-dialog\"\n        :visible=\"visible\"\n        @close=\"onClose\"\n        @ok=\"onClose\"\n        @cancel=\"onClose\"\n        unmountOnClose\n    >\n        <template #title>\n            <div class=\"flex item-center\" style=\"width: 300px\">\n                <div class=\"w-10 bg-gray-100 rounded-lg mr-2\">\n                    <img :src=\"record?.plugin.logo\" />\n                </div>\n                <div class=\"flex-grow\">\n                    <div class=\"font-bold text-sm\">\n                        {{ record?.plugin.title }}\n                    </div>\n                    <div class=\"text-gray-400 text-sm\">\n                        {{ record?.count }} {{ $t(\"data.docCount\") }}\n                    </div>\n                </div>\n            </div>\n        </template>\n        <template #footer>\n            <a-button\n                type=\"primary\"\n                size=\"small\"\n                status=\"danger\"\n                @click=\"doTruncate()\"\n            >\n                <template #icon>\n                    <icon-delete />\n                </template>\n                {{ $t(\"data.clear\") }}\n            </a-button>\n            <a-button size=\"small\" @click=\"onClose()\">\n                {{ $t(\"common.close\") }}\n            </a-button>\n        </template>\n        <div style=\"margin: -12px -16px; height: calc(100% + 24px)\">\n            <div class=\"h-full\">\n                <m-empty v-if=\"!loading && keys.length === 0\" />\n                <div\n                    v-for=\"k in keys\"\n                    @click=\"\n                        dataViewDetailDialog?.show(\n                            record as SystemDataRecord,\n                            k,\n                        )\n                    \"\n                    class=\"border-b border-solid border-default p-2 truncate w-full hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer\"\n                >\n                    {{ k }}\n                </div>\n            </div>\n        </div>\n    </a-drawer>\n    <SystemDataViewDetailDialog ref=\"dataViewDetailDialog\" @update=\"onUpdate\" />\n</template>\n\n<style lang=\"less\">\n.pb-system-data-view-dialog .arco-drawer-header {\n    height: auto;\n    padding: 1rem;\n}\n</style>\n"
  },
  {
    "path": "src/pages/System/components/type.ts",
    "content": "import { PluginRecord } from \"../../../types/Manager\";\n\nexport type SystemDataRecord = {\n    plugin: PluginRecord;\n    count: 0;\n};\n"
  },
  {
    "path": "src/router.ts",
    "content": "import { createRouter, createWebHashHistory } from \"vue-router\";\n\nconst routes = [\n    {\n        path: \"/\",\n        component: () => import(\"./layouts/Main.vue\"),\n        children: [\n            { path: \"\", component: () => import(\"./pages/Home.vue\") },\n            { path: \"setting\", component: () => import(\"./pages/Setting.vue\") },\n        ],\n    },\n    {\n        path: \"/\",\n        component: () => import(\"./layouts/Raw.vue\"),\n        children: [],\n    },\n];\n\nconst router = createRouter({\n    history: createWebHashHistory(),\n    routes,\n});\n\n// watch router change\nrouter.beforeEach((to, from, next) => {\n    window.$mapi?.statistics?.tick(\"visit\", {\n        path: to.path,\n    });\n    next();\n});\n\nexport default router;\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import { createPinia } from \"pinia\";\n\nconst store = createPinia();\nexport default store;\n"
  },
  {
    "path": "src/store/modules/app.ts",
    "content": "import { defineStore } from \"pinia\";\nimport store from \"../index\";\n\nexport const appStore = defineStore(\"app\", {\n    state() {\n        return {};\n    },\n    actions: {\n        async init() {},\n    },\n});\n\nexport const app = appStore(store);\napp.init().then(() => {});\n\nexport const useAppStore = () => {\n    return app;\n};\n"
  },
  {
    "path": "src/store/modules/manager.ts",
    "content": "import debounce from \"lodash/debounce\";\nimport { defineStore } from \"pinia\";\nimport { computed, toRaw } from \"vue\";\nimport { WindowConfig } from \"../../../electron/config/window\";\nimport { t } from \"../../lang\";\nimport {\n    ActionRecord,\n    ActionTypeEnum,\n    ConfigRecord,\n    PluginRecord,\n} from \"../../types/Manager\";\nimport store from \"../index\";\n\nconst searchFastPanelActionDebounce = debounce((query, cb) => {\n    window.$mapi.manager.searchFastPanelAction(query).then((result) => {\n        cb(result);\n    });\n});\n\nconst searchDebounce = debounce((query, cb) => {\n    window.$mapi.manager.searchAction(query).then((result) => {\n        cb(result);\n    });\n}, 300);\n\nconst subInputChangeDebounce = debounce((keywords) => {\n    window.$mapi.manager.subInputChange(keywords);\n}, 300);\n\nconst searchActionCodeDebounce = debounce((keywords) => {\n    window.$mapi.manager.searchActionCode(keywords).then();\n}, 300);\n\nexport const managerStore = defineStore(\"manager\", {\n    state: () => ({\n        config: {} as ConfigRecord,\n        showFirstRun: false,\n        searchLoading: false,\n        searchLastKeywords: \"\",\n        searchValue: \"\",\n        searchPlaceholder: t(\"main.placeholder\"),\n        searchSubPlaceholder: \"\",\n        searchSubIsVisible: false,\n        searchIsCompositing: false,\n\n        detachWindowActions: [] as ActionRecord[],\n        searchActions: [] as ActionRecord[],\n        matchActions: [] as ActionRecord[],\n        historyActions: [] as ActionRecord[],\n        pinActions: [] as ActionRecord[],\n        viewActions: [] as ActionRecord[],\n\n        selectedAction: null as ActionRecord | null,\n        activePlugin: null as PluginRecord | null,\n        activePluginType: null as \"code\" | null,\n        activePluginLoading: false,\n\n        actionCodeLoading: false,\n        actionCodeError: null as string | null,\n        actionCodeType: null as \"list\" | null,\n        actionCodeItemActiveId: null as string | null,\n        actionCodeItems: [] as {\n            id: string;\n            shortcutIndex: number;\n            [key: string]: any;\n        }[],\n\n        currentFiles: [] as FileItem[],\n        currentImage: \"\",\n        currentText: \"\",\n\n        fastPanelActionLoading: false,\n        fastPanelMatchActions: [] as ActionRecord[],\n        fastPanelViewActions: [] as ActionRecord[],\n\n        notice: null as {\n            text: string;\n            type: \"info\" | \"error\" | \"success\";\n            duration: number;\n        } | null,\n        noticeCleanTimer: null as any,\n    }),\n    actions: {\n        async init() {\n            this.config = await window.$mapi.manager.getConfig();\n        },\n\n        async setConfig(key: string, value: any) {\n            // console.log('setConfig', key, value, toRaw(this.config))\n            this.config[key] = value;\n            await window.$mapi.manager.setConfig(toRaw(this.config));\n        },\n        async onConfigChange(key: string, value: any) {\n            return await this.setConfig(key, toRaw(value));\n        },\n        configGet(key: string, defaultValue: any = null) {\n            return computed(() => {\n                if (key in this.config) {\n                    return this.config[key];\n                }\n                return defaultValue;\n            });\n        },\n        setActivePluginLoading(loading: boolean) {\n            this.activePluginLoading = loading;\n        },\n        setActivePlugin(\n            plugin: PluginRecord | null,\n            type: \"code\" | null = null,\n        ) {\n            this.activePlugin = plugin;\n            this.activePluginType = plugin ? type : null;\n        },\n        setSearchValue(value: string) {\n            if (this.activePlugin) {\n                return;\n            }\n            this.searchValue = value;\n        },\n        setSelectedAction(action: ActionRecord) {\n            this.selectedAction = action;\n            document\n                .querySelector(`[data-action=\"${action.fullName}\"]`)\n                ?.scrollIntoView({\n                    behavior: \"smooth\",\n                    block: \"center\",\n                    inline: \"center\",\n                });\n        },\n        setCurrentFiles(files: FileItem[]) {\n            this.currentFiles = files;\n        },\n        setCurrentImage(image: string) {\n            this.currentImage = image;\n        },\n        setCurrentText(text: string) {\n            this.currentText = text;\n        },\n\n        async searchFastPanel(keywords: string) {\n            this.fastPanelMatchActions = [];\n            this.fastPanelViewActions = [];\n            this.fastPanelActionLoading = true;\n            searchFastPanelActionDebounce(\n                {\n                    keywords: keywords,\n                    currentFiles: toRaw(this.currentFiles),\n                    currentImage: this.currentImage,\n                    currentText: this.currentText,\n                },\n                (result: {\n                    matchActions: ActionRecord[];\n                    viewActions: ActionRecord[];\n                }) => {\n                    this.fastPanelMatchActions = result.matchActions;\n                    this.fastPanelViewActions = result.viewActions;\n                    this.fastPanelActionLoading = false;\n                },\n            );\n        },\n\n        async searchRefresh() {\n            await this.search(this.searchLastKeywords);\n        },\n\n        async search(keywords: string) {\n            if (this.activePlugin) {\n                if (this.activePluginType === \"code\") {\n                    this.searchValue = keywords;\n                    searchActionCodeDebounce(keywords);\n                    return;\n                }\n                subInputChangeDebounce(keywords);\n                this.searchValue = keywords;\n                return;\n            }\n            this.searchLoading = true;\n            this.searchValue = keywords;\n            this.viewActions = [];\n            searchDebounce(\n                {\n                    keywords,\n                    currentFiles: toRaw(this.currentFiles),\n                    currentImage: this.currentImage,\n                    currentText: this.currentText,\n                },\n                (result: {\n                    detachWindowActions: ActionRecord[];\n                    searchActions: ActionRecord[];\n                    matchActions: ActionRecord[];\n                    viewActions: ActionRecord[];\n                    historyActions: ActionRecord[];\n                    pinActions: ActionRecord[];\n                }) => {\n                    this.searchLastKeywords = keywords;\n                    this.detachWindowActions = result.detachWindowActions;\n                    this.searchActions = result.searchActions;\n                    this.matchActions = result.matchActions;\n                    this.viewActions = result.viewActions;\n                    this.historyActions = result.historyActions;\n                    this.pinActions = result.pinActions;\n                    this.searchLoading = false;\n                },\n            );\n        },\n        async detachWindowActionsRefresh() {\n            this.detachWindowActions =\n                await window.$mapi.manager.listDetachWindowActions();\n        },\n        async resize(width: number, height: number) {\n            height = Math.min(height, WindowConfig.mainMaxHeight);\n            await window.$mapi.app.windowSetSize(\n                null,\n                WindowConfig.mainWidth,\n                height,\n                {\n                    center: false,\n                },\n            );\n        },\n        async isMainWindowShown() {\n            return await window.$mapi.manager.isShown();\n        },\n        async showMainWindow() {\n            await window.$mapi.manager.show();\n        },\n        async hideMainWindow() {\n            await window.$mapi.manager.hide();\n        },\n        async openAction(\n            action: ActionRecord,\n            group: undefined | \"window\" = undefined,\n        ) {\n            await window.$mapi.manager.openAction(toRaw(action));\n            if (\n                action.type === ActionTypeEnum.COMMAND ||\n                action.type === ActionTypeEnum.BACKEND\n            ) {\n                await window.$mapi.manager.hide();\n            }\n            this.searchValue = \"\";\n            // this.detachWindowActions = [];\n            // this.searchActions = [];\n            // this.matchActions = [];\n            // this.viewActions = [];\n            // this.historyActions = [];\n            // this.pinActions = [];\n        },\n        async openActionCode(id: string) {\n            if (!this.activePlugin || this.activePluginType !== \"code\") {\n                return;\n            }\n            this.actionCodeItemActiveId = id;\n            await window.$mapi.manager.openActionCode(id);\n        },\n        async openActionWindow(type: \"open\", action: ActionRecord) {\n            await window.$mapi.manager.openActionWindow(type, toRaw(action));\n        },\n        async closeMainPlugin() {\n            await window.$mapi.manager.closeMainPlugin();\n        },\n        async openMainPluginDevTools() {\n            await window.$mapi.manager.openMainPluginDevTools();\n        },\n        async openMainPluginLog() {\n            await window.$mapi.manager.openMainPluginLog();\n        },\n        async detachPlugin() {\n            await window.$mapi.manager.detachPlugin();\n        },\n        setSubInput(payload: {\n            placeholder: string;\n            isFocus: boolean;\n            isVisible: boolean;\n        }) {\n            if (!this.activePlugin) {\n                return;\n            }\n            this.searchSubPlaceholder = payload.placeholder || \"\";\n            this.searchSubIsVisible = payload.isVisible || false;\n        },\n        removeSubInput() {\n            if (!this.activePlugin) {\n                return;\n            }\n            this.searchSubPlaceholder = \"\";\n            this.searchSubIsVisible = false;\n            this.searchValue = \"\";\n        },\n        setSubInputValue(value: string) {\n            if (!this.activePlugin) {\n                return;\n            }\n            this.searchValue = value;\n        },\n        onNotice(data: any) {\n            this.notice = data;\n            if (this.notice?.duration && this.notice?.duration > 0) {\n                if (this.noticeCleanTimer) {\n                    clearTimeout(this.noticeCleanTimer);\n                }\n                this.noticeCleanTimer = setTimeout(() => {\n                    this.notice = null;\n                }, this.notice.duration);\n            }\n        },\n    },\n});\n\nconst manager = managerStore(store);\nmanager.init().then();\n\nwindow.__page.onBroadcast(\"Notice\", manager.onNotice);\n\nexport const useManagerStore = () => {\n    return manager;\n};\n"
  },
  {
    "path": "src/store/modules/setting.ts",
    "content": "import { cloneDeep } from \"lodash-es\";\nimport { defineStore } from \"pinia\";\nimport { computed } from \"vue\";\nimport { AppConfig } from \"../../config\";\nimport { applyLocale } from \"../../lang\";\nimport store from \"../index\";\n\nexport const settingStore = defineStore(\"setting\", {\n    state() {\n        return {\n            version: AppConfig.version,\n            basic: cloneDeep(AppConfig.basic),\n            isDarkMode: false,\n            buildInfo: {\n                buildId: \"\",\n            },\n            config: {\n                guideWatched: false as boolean,\n                darkMode: \"\" as \"light\" | \"dark\" | \"auto\",\n            },\n            configEnv: {},\n        };\n    },\n    actions: {\n        async init() {\n            this.isDarkMode = await window.$mapi.app.isDarkMode();\n            this.config = await window.$mapi.config.all();\n            this.configEnv = await window.$mapi.config.allEnv();\n            this.setupDarkMode();\n            this.showGuideWhenReady().then();\n            window.$mapi.app.getBuildInfo().then((info: any) => {\n                this.buildInfo = info;\n            });\n        },\n        async showGuideWhenReady() {\n            if (!(await window.$mapi.app.setupIsOk())) {\n                setTimeout(() => {\n                    this.showGuideWhenReady();\n                }, 1000);\n                return;\n            }\n            setTimeout(() => {\n                if (!this.config.guideWatched) {\n                    window.$mapi.app.windowOpen(\"guide\").then();\n                    this.setConfig(\"guideWatched\", true).then();\n                }\n            }, 2000);\n        },\n        onConfigChangeBroadcast(data: any) {\n            (async () => {\n                this.config = await window.$mapi.config.all();\n                this.setupDarkMode();\n                if (data?.key === \"lang\" && data?.value) {\n                    applyLocale(data.value);\n                }\n            })();\n        },\n        onConfigEnvChangeBroadcast(data: any) {\n            (async () => {\n                this.configEnv = await window.$mapi.config.allEnv();\n            })();\n        },\n        onDarkModeChangeBroadcast(data: any) {\n            this.isDarkMode = data.isDarkMode;\n            this.setupDarkMode();\n        },\n        shouldDarkMode() {\n            const darkMode = this.config[\"darkMode\"] || \"auto\";\n            if (\"dark\" === darkMode) {\n                return true;\n            } else if (\"light\" === darkMode) {\n                return false;\n            } else if (\"auto\" === darkMode) {\n                return this.isDarkMode;\n            }\n            return false;\n        },\n        setupDarkMode() {\n            // console.log('setupDarkMode')\n            if (this.shouldDarkMode()) {\n                document.body.setAttribute(\"arco-theme\", \"dark\");\n                document.body.setAttribute(\"data-theme\", \"dark\");\n                document.documentElement.setAttribute(\"data-theme\", \"dark\");\n            } else {\n                document.body.removeAttribute(\"arco-theme\");\n                document.body.removeAttribute(\"data-theme\");\n                document.documentElement.removeAttribute(\"data-theme\");\n            }\n        },\n        async initBasic(basic: object) {\n            this.basic = Object.assign(this.basic, basic);\n        },\n        async setConfig(key: string, value: any) {\n            // console.log('setConfig', key, value)\n            this.config[key] = value;\n            await window.$mapi.config.set(key, value);\n            if (\"darkMode\" === key) {\n                setTimeout(() => this.setupDarkMode(), 100);\n            }\n        },\n        async setConfigEnv(key: string, value: any) {\n            this.configEnv[key] = value;\n            await window.$mapi.config.setEnv(key, value);\n        },\n        async onConfigChange(key: string, value: any) {\n            return await this.setConfig(key, value);\n        },\n        async onConfigEnvChange(key: string, value: any) {\n            return await this.setConfigEnv(key, value);\n        },\n        configGet(key: string, defaultValue: any = null) {\n            return computed(() => {\n                if (key in this.config) {\n                    return this.config[key];\n                }\n                return defaultValue;\n            });\n        },\n        configEnvGet(key: string, defaultValue: any = null) {\n            return computed(() => {\n                if (key in this.configEnv) {\n                    return this.configEnv[key];\n                }\n                return defaultValue;\n            });\n        },\n    },\n});\n\nconst setting = settingStore(store);\nsetting.init().then();\n\nwindow.__page.onBroadcast(\"ConfigChange\", setting.onConfigChangeBroadcast);\nwindow.__page.onBroadcast(\n    \"ConfigEnvChange\",\n    setting.onConfigEnvChangeBroadcast,\n);\nwindow.__page.onBroadcast(\"DarkModeChange\", setting.onDarkModeChangeBroadcast);\n\nexport const useSettingStore = () => {\n    return setting;\n};\n"
  },
  {
    "path": "src/store/modules/task.ts",
    "content": "// ------------------------------------------------------------------------------\n// ----------------------------- Task Schedule Store ----------------------------\n// ------------------------------------------------------------------------------\n// Register TaskBiz\n//   taskStore.register('TestSync', TestSync)\n//   taskStore.register('TestAsync', TestAsync)\n// Dispatch Task\n//   await taskStore.dispatch('TestSync', StringUtil.random())\n//   await taskStore.dispatch('TestAsync', StringUtil.random(), {'a':1}, {timeout: 3 * 1000})\n// Schedule call order\n//   Sync Task runFunc -> successFunc | failFunc\n//   Async Task runFunc -> queryFunc -> successFunc | failFunc\n// ------------------------------------------------------------------------------\n// ------------------------------------------------------------------------------\n// ------------------------------------------------------------------------------\n\nimport { cloneDeep } from \"lodash-es\";\nimport { defineStore } from \"pinia\";\nimport { toRaw } from \"vue\";\nimport { mapError } from \"../../lib/error\";\nimport { StringUtil, TimeUtil } from \"../../lib/util\";\nimport store from \"../index\";\n\nexport type TaskRecordStatus =\n    | \"queue\"\n    | \"running\"\n    | \"querying\"\n    | \"success\"\n    | \"fail\"\n    | \"delete\";\n\nexport type TaskRecordRunStatus = \"retry\" | \"success\" | \"querying\";\n\nexport type TaskRecordQueryStatus = \"running\" | \"success\" | \"fail\";\n\nexport type TaskChangeType =\n    | \"running\"\n    | \"success\"\n    | \"fail\"\n    | \"change\"\n    | \"requestCancel\";\n\nexport type TaskRecord = {\n    id: string;\n    status: TaskRecordStatus;\n    msg: string;\n    biz: string;\n    bizId: string;\n    bizParam: any;\n    // 开始运行时间\n    runStart: number;\n    // 是否正在调用 runFunc\n    runCalling: boolean;\n    // 超过 runAfter 才会执行，0表示是一个新任务，＞0表示是一个重试任务\n    runAfter: number;\n    // 是否正在调用 queryFunc\n    queryCalling: boolean;\n    // 超过 queryAfter 才会查询\n    queryAfter: number;\n    // 查询间隔\n    queryInterval: number;\n    // 是否正在调用 successFunc\n    successCalling: boolean;\n    // 超时时间\n    timeout: number;\n};\n\nexport type TaskBiz = {\n    // sync task run,  return success | retry\n    // async task run, return querying | success | retry\n    runFunc: (bizId: string, bizParam: any) => Promise<TaskRecordRunStatus>;\n    // async task query status, return running | success | fail\n    queryFunc?: (\n        bizId: string,\n        bizParam: any,\n    ) => Promise<TaskRecordQueryStatus>;\n    // sync task success callback\n    // async task success callback\n    successFunc: (bizId: string, bizParam: any) => Promise<void>;\n    // Make sure the \"failFunc\" function always not throw an error\n    failFunc: (bizId: string, msg: string, bizParam: any) => Promise<void>;\n    // request cancel callback, when user request cancel a task, will call this function\n    requestCancelFunc?: (bizId: string, bizParam: any) => Promise<void>;\n    // ----------------------------------------------------\n    // the following not use in schedule, only for biz\n    [key: string]: any;\n    // ----------------------------------------------------\n};\n\nconst taskChangeListeners = [] as {\n    biz: string | null;\n    callback: (bizId: string, status: TaskChangeType) => void;\n}[];\nlet runNextTimer = null as any;\n\nexport const TestSync: TaskBiz = {\n    runFunc: async (bizId, bizParam) => {\n        console.log(\"Task.TestSync.runFunc\", { bizId, bizParam });\n        return \"success\";\n    },\n    successFunc: async (bizId, bizParam) => {\n        console.log(\"Task.TestSync.successFunc\", { bizId, bizParam });\n    },\n    failFunc: async (bizId, msg, bizParam) => {\n        console.log(\"Task.TestSync.failFunc\", { bizId, bizParam, msg });\n    },\n};\nexport const TestAsync: TaskBiz = {\n    runFunc: async (bizId, bizParam) => {\n        console.log(\"Task.TestAsync.runFunc\", { bizId, bizParam });\n        return \"querying\";\n    },\n    queryFunc(bizId, bizParam) {\n        return new Promise((resolve) => {\n            console.log(\"Task.TestAsync.queryFunc\", { bizId, bizParam });\n            setTimeout(() => {\n                resolve(Math.random() > 0.7 ? \"success\" : \"running\");\n            }, 1000);\n        });\n    },\n    successFunc: async (bizId, bizParam) => {\n        console.log(\"Task.TestAsync.successFunc\", { bizId, bizParam });\n    },\n    failFunc: async (bizId, msg, bizParam) => {\n        console.log(\"Task.TestAsync.failFunc\", { bizId, bizParam, msg });\n    },\n};\n\nexport const taskStore = defineStore(\"task\", {\n    state() {\n        return {\n            isInit: false,\n            bizMap: {} as Record<string, TaskBiz>,\n            records: [] as TaskRecord[],\n            cancelMap: {} as Record<\n                string,\n                {\n                    expire: number;\n                }\n            >,\n        };\n    },\n    actions: {\n        async init() {\n            await $mapi.storage.get(\"task\", \"records\", []).then((records) => {\n                this.records = records;\n                this.isInit = true;\n                this._run(true);\n            });\n        },\n        async waitInit() {\n            while (!this.isInit) {\n                await new Promise((resolve) => setTimeout(resolve, 100));\n            }\n        },\n        _runExecute() {\n            let changed = false;\n            // console.log('task._runExecute.start', JSON.stringify(this.records))\n            // error record\n            this.records.forEach((record) => {\n                if (!this.bizMap[record.biz]) {\n                    record.status = \"fail\";\n                    record.msg = \"biz not found\";\n                    changed = true;\n                }\n            });\n            // console.log('task.records', JSON.stringify(this.records, null, 2))\n            // request cancel\n            this.records\n                .filter(\n                    (r) =>\n                        r.status === \"queue\" ||\n                        r.status === \"running\" ||\n                        r.status === \"querying\",\n                )\n                .filter((r) => this.shouldCancel(r.biz, r.bizId))\n                .forEach((record) => {\n                    record.status = \"fail\";\n                    record.msg = mapError(\"UserCancel\");\n                    changed = true;\n                });\n            // queue\n            this.records\n                .filter((r) => r.status === \"queue\")\n                .filter((r) => r.runAfter <= Date.now() && !r.runCalling)\n                .forEach((record) => {\n                    changed = true;\n                    record.status = \"running\";\n                    record.runStart = Date.now();\n                    record.runCalling = true;\n                    let runCallFinish = false;\n                    setTimeout(() => {\n                        if (runCallFinish) {\n                            return;\n                        }\n                        this.fireChange(record, \"running\");\n                    }, 1000);\n                    this.bizMap[record.biz]\n                        .runFunc(record.bizId, record.bizParam)\n                        .then((status: TaskRecordRunStatus) => {\n                            runCallFinish = true;\n                            switch (status) {\n                                case \"success\":\n                                    record.status = \"success\";\n                                    break;\n                                case \"querying\":\n                                    record.queryAfter =\n                                        Date.now() + record.queryInterval;\n                                    record.status = \"querying\";\n                                    break;\n                                case \"retry\":\n                                    record.status = \"queue\";\n                                    record.runStart = 0;\n                                    record.runAfter = Date.now() + 1000;\n                                    break;\n                            }\n                        })\n                        .catch((e) => {\n                            runCallFinish = true;\n                            record.status = \"fail\";\n                            record.msg = mapError(e);\n                            console.error(\"Task.RunFunc.Error\", e);\n                            $mapi.log\n                                .error(\"Task.RunFunc.Error\", e.toString())\n                                .catch((e) => {\n                                    console.error(\"Task.RunFunc.Error.Log\", e);\n                                });\n                        })\n                        .finally(() => {\n                            record.runCalling = false;\n                            this.fireChange(record, \"running\");\n                        });\n                });\n            // querying\n            this.records\n                .filter((r) => r.status === \"querying\")\n                .filter((r) => r.queryAfter <= Date.now() && !r.queryCalling)\n                .forEach((record) => {\n                    record.queryCalling = true;\n                    const taskBiz = this.bizMap[record.biz];\n                    taskBiz\n                        .queryFunc?.(record.bizId, record.bizParam)\n                        .then((status: TaskRecordQueryStatus) => {\n                            switch (status) {\n                                case \"running\":\n                                    record.queryAfter =\n                                        Date.now() + record.queryInterval;\n                                    break;\n                                case \"success\":\n                                    record.status = \"success\";\n                                    changed = true;\n                                    break;\n                                case \"fail\":\n                                    record.status = \"fail\";\n                                    changed = true;\n                                    break;\n                            }\n                        })\n                        .catch((e) => {\n                            record.status = \"fail\";\n                            record.msg = mapError(e);\n                            changed = true;\n                            console.error(\"Task.QueryFunc.Error\", e);\n                            $mapi.log\n                                .error(\"Task.QueryFunc.Error\", e.toString())\n                                .catch((e) => {\n                                    console.error(\n                                        \"Task.QueryFunc.Error.Log\",\n                                        e,\n                                    );\n                                });\n                        })\n                        .finally(() => {\n                            record.queryCalling = false;\n                        });\n                });\n            // expire\n            this.records\n                .filter(\n                    (r) => r.status === \"running\" || r.status === \"querying\",\n                )\n                .filter((r) => Date.now() - r.runStart > r.timeout)\n                .forEach((record) => {\n                    record.status = \"fail\";\n                    record.msg = mapError(\"ProcessTimeout\");\n                    changed = true;\n                });\n            // success\n            this.records\n                .filter((r) => r.status === \"success\")\n                .filter((r) => !r.successCalling)\n                .forEach((record) => {\n                    record.successCalling = true;\n                    changed = true;\n                    this.bizMap[record.biz]\n                        .successFunc(record.bizId, record.bizParam)\n                        .then(() => {\n                            record.status = \"delete\";\n                        })\n                        .catch((e) => {\n                            console.error(\"Task.SuccessFunc.Error\", e);\n                            $mapi.log\n                                .error(\"Task.SuccessFunc.Error\", e.toString())\n                                .catch((e) => {\n                                    console.error(\n                                        \"Task.SuccessFunc.Error.Log\",\n                                        e,\n                                    );\n                                });\n                            record.status = \"fail\";\n                            record.msg = mapError(e);\n                        })\n                        .finally(() => {\n                            if (record.status === \"delete\") {\n                                this.fireChange(record, \"success\");\n                            }\n                            record.successCalling = false;\n                        });\n                });\n            // fail\n            this.records\n                .filter((r) => r.status === \"fail\")\n                .forEach((record) => {\n                    changed = true;\n                    record.status = \"delete\";\n                    if (!this.bizMap[record.biz]) {\n                        return;\n                    }\n                    this.bizMap[record.biz]\n                        .failFunc(record.bizId, record.msg, record.bizParam)\n                        .then(() => {})\n                        .catch((e) => {\n                            console.error(\"Task.FailFunc.Error\", e);\n                            $mapi.log\n                                .error(\"Task.FailFunc.Error\", e.toString())\n                                .catch((e) => {\n                                    console.error(\"Task.FailFunc.Error.Log\", e);\n                                });\n                        })\n                        .finally(() => {\n                            this.fireChange(record, \"fail\");\n                        });\n                });\n            // console.log('task._runExecute.end', JSON.stringify(this.records))\n            // delete\n            this.records = this.records.filter((r) => r.status !== \"delete\");\n            // sync\n            if (changed) {\n                this.sync().then();\n            }\n            // next run\n            // console.log('run', changed, JSON.stringify(this.records))\n            if (this.records.length > 0) {\n                this._run(changed);\n            }\n        },\n        _run(immediate: boolean) {\n            if (runNextTimer) {\n                clearTimeout(runNextTimer);\n                runNextTimer = null;\n            }\n            setTimeout(\n                () => {\n                    this._runExecute();\n                },\n                immediate ? 0 : 1000,\n            );\n        },\n        get(biz: string) {\n            return this.bizMap[biz] || null;\n        },\n        register(biz: string, taskBiz: TaskBiz) {\n            this.bizMap[biz] = taskBiz;\n        },\n        unregister(biz: string) {\n            delete this.bizMap[biz];\n        },\n        onChange(\n            biz: string | null,\n            callback: (bizId: string, type: TaskChangeType) => void,\n        ) {\n            taskChangeListeners.push({ biz, callback });\n        },\n        offChange(\n            biz: string | null,\n            callback: (bizId: string, type: TaskChangeType) => void,\n        ) {\n            const index = taskChangeListeners.findIndex(\n                (v) => v.biz === biz && v.callback === callback,\n            );\n            taskChangeListeners.splice(index, 1);\n        },\n        fireChange(record: Partial<TaskRecord>, type: TaskChangeType) {\n            taskChangeListeners.forEach((v) => {\n                if (null === v.biz || v.biz === record.biz) {\n                    v.callback(record.bizId as string, type);\n                }\n            });\n        },\n        requestCancel(biz: string, bizId: string) {\n            this.cancelMap[`${biz}-${bizId}`] = {\n                expire: TimeUtil.timestampMS() + 60 * 60 * 1000,\n            };\n            this.fireChange({ biz, bizId }, \"requestCancel\");\n            if (this.bizMap[biz]?.requestCancelFunc) {\n                this.bizMap[biz]?.requestCancelFunc?.(bizId, {}).catch((e) => {\n                    $mapi.log\n                        .error(\"Task.RequestCancelFunc.Error\", e.toString())\n                        .then();\n                });\n            }\n        },\n        shouldCancel(biz: string, bizId: string) {\n            // expire old\n            for (const key in this.cancelMap) {\n                if (this.cancelMap[key].expire < TimeUtil.timestampMS()) {\n                    delete this.cancelMap[key];\n                }\n            }\n            if (!!this.cancelMap[`${biz}-${bizId}`]) {\n                delete this.cancelMap[`${biz}-${bizId}`];\n                return true;\n            }\n            return false;\n        },\n        async dispatch(\n            biz: string,\n            bizId: string,\n            bizParam?: any,\n            param?: object,\n        ) {\n            await this.waitInit();\n            if (!this.bizMap[biz]) {\n                throw new Error(\"TaskBizNotFound\");\n            }\n            param = Object.assign(\n                {\n                    timeout: 24 * 60 * 60 * 1000,\n                    queryInterval: 5 * 1000,\n                    status: \"queue\",\n                    runStart: 0,\n                },\n                param,\n            );\n            const taskRecord = {\n                id: `${biz}-${Date.now()}-${StringUtil.random(8)}`,\n                status: param[\"status\"],\n                msg: \"\",\n                biz,\n                bizId,\n                bizParam,\n                runStart: param[\"runStart\"],\n                runAfter: 0,\n                runCalling: false,\n                queryAfter: 0,\n                queryInterval: param[\"queryInterval\"],\n                queryCalling: false,\n                successCalling: false,\n                timeout: param[\"timeout\"],\n            } as TaskRecord;\n            this.records.push(taskRecord);\n            this._run(true);\n        },\n        async sync() {\n            await this.waitInit();\n            const savedRecords = toRaw(cloneDeep(this.records));\n            savedRecords.forEach((record) => {\n                // record.status = undefined\n                // record.runtime = undefined\n            });\n            await $mapi.storage.set(\"task\", \"records\", savedRecords);\n        },\n    },\n});\n\nexport const task = taskStore(store);\ntask.init().then();\n\nexport const useTaskStore = () => {\n    return task;\n};\n"
  },
  {
    "path": "src/store/modules/user.ts",
    "content": "import { defineStore } from \"pinia\";\nimport store from \"../index\";\nimport { AppConfig } from \"../../config\";\nimport { useSettingStore } from \"./setting\";\n\nconst setting = useSettingStore();\n\nexport const userStore = defineStore(\"user\", {\n    state() {\n        return {\n            isInit: false,\n            lastSavedJson: \"\",\n            apiToken: null as string | null,\n            user: {\n                id: null as string | null,\n                name: null as string | null,\n                avatar: null as string | null,\n            },\n            data: {\n                vip: {},\n                functions: {},\n            } as {\n                vip: {\n                    [key: string]: any;\n                };\n                functions: {\n                    [key: string]: any;\n                };\n                [key: string]: any;\n            },\n            basic: {} as {\n                [key: string]: any;\n            },\n        };\n    },\n    actions: {\n        async init() {\n            await this.load();\n        },\n        async load() {\n            const { apiToken, user, data, basic } =\n                await window.$mapi.user.get();\n            this.apiToken = apiToken;\n            this.user = Object.assign(this.user, user);\n            this.data = data as any;\n            this.basic = basic;\n            await setting.initBasic(this.basic);\n            this.isInit = true;\n        },\n        onChangeBroadcast() {\n            this.load().then();\n        },\n        async waitInit() {\n            if (this.isInit) {\n                return;\n            }\n            await new Promise((resolve) => {\n                const timer = setInterval(() => {\n                    if (this.isInit) {\n                        clearInterval(timer);\n                        resolve(undefined);\n                    }\n                }, 100);\n            });\n        },\n        async webUrl() {\n            await this.waitInit();\n            let param: string[] = [];\n            if (this.apiToken) {\n                param.push(`api_token=${this.apiToken}`);\n            }\n            if (setting.shouldDarkMode()) {\n                param.push(\"is_dark=1\");\n            }\n            return `${AppConfig.apiBaseUrl}/app_manager/user_web?${param.join(\"&\")}`;\n        },\n    },\n});\n\nexport const user = userStore(store);\n\nuser.init().then();\n\nwindow.__page.onBroadcast(\"UserChange\", user.onChangeBroadcast);\n\nexport const useUserStore = () => {\n    return user;\n};\n"
  },
  {
    "path": "src/style.less",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root, body {\n    --primary-1: 255, 255, 255;\n    --primary-2: 207, 207, 207;\n    --primary-3: 160, 160, 160;\n    --primary-4: 112, 112, 112;\n    --primary-5: 65, 65, 65;\n    --primary-6: 17, 17, 17;\n    --primary-7: 17, 13, 13;\n    --primary-8: 17, 9, 9;\n    --primary-9: 17, 4, 6;\n    --primary-10: 17, 0, 2;\n\n    --color-primary-light-1: rgb(var(--primary-1));\n    --color-primary-light-2: rgb(var(--primary-2));\n    --color-primary-light-3: rgb(var(--primary-3));\n    --color-primary-light-4: rgb(var(--primary-4));\n\n    --window-header-height: 2.5rem;\n    --page-nav-width: 4rem;\n\n    //--color-primary: #5154E0; // 41\t85\t254\n    --color-primary: #265BD7;\n    // --color-primary-lighter: #6A6DFF;\n    --color-primary-lighter: lighten(#265BD7, 10%);\n    //--color-bg-page-nav: #2A3A5D;\n    --color-bg-page-nav: #FFFFFF;\n    //--color-bg-page-nav-active: #5154E0;\n    --color-bg-page-nav-active: #FFFFFF;\n    //--color-text-page-nav: #FFFFFF;\n    --color-text-page-nav: #000000;\n    //--color-text-page-nav-active: #EEEEEE;\n    --color-text-page-nav-active: #265BD7;\n    //--color-border-page-nav: #2A3A5D;\n    --color-border-page-nav: #EEEEEE;\n\n    --color-background: #FFFFFF;\n    --color-background-content: #ededed;\n    --color-text: #111111;\n    --color-border: #E5E6EB;\n}\n\nbody[data-theme=\"dark\"] {\n    --primary-1: 17, 0, 2;\n    --primary-2: 17, 4, 6;\n    --primary-3: 17, 9, 9;\n    --primary-4: 17, 13, 13;\n    --primary-5: 17, 17, 17;\n    --primary-6: 65, 65, 65;\n    --primary-7: 112, 112, 112;\n    --primary-8: 160, 160, 160;\n    --primary-9: 207, 207, 207;\n    --primary-10: 255, 255, 255;\n\n    --color-background: #17171A;\n    --color-background-content: #333333;\n    --color-text: #CCCCCC;\n    --color-border: #484849;\n    --color-text-page-nav: #CCCCCC;\n    --color-bg-page-nav: #17171A;\n    --color-bg-page-nav-active: #2d3443;\n}\n\nbody {\n    overflow: hidden;\n}\n\nhtml {\n    background-color: transparent;\n}\n\nbody {\n    background-color: var(--color-background);\n    font-size: 14px;\n    color: var(--color-text);\n}\n\n/////////// layout start ///////////\n\n.page-container {\n    height: calc(100vh - var(--window-header-height));\n    width: 100vw;\n}\n\n.page-narrow-container {\n    max-width: 100rem;\n    margin: 0 auto;\n}\n\n.page-nav-item {\n    color: var(--color-text-page-nav);\n\n    &.active {\n        background: var(--color-bg-page-nav-active);\n        color: var(--color-text-page-nav-active);\n    }\n}\n\n.window-header {\n    -webkit-user-select: none;\n\n    .window-header-title {\n        -webkit-app-region: drag;\n    }\n}\n\n\n* {\n    &::-webkit-scrollbar {\n        width: 6px;\n        height: 6px;\n    }\n\n    &::-webkit-scrollbar-track {\n        background: #FFFFFF;\n    }\n\n    &::-webkit-scrollbar-thumb {\n        background: #DDDDDD;\n        border-radius: 10px;\n    }\n\n    &::-webkit-scrollbar-thumb:hover {\n        background: #CCCCCC;\n    }\n}\n\nbody[data-theme=\"dark\"] {\n    * {\n        &::-webkit-scrollbar-track {\n            background: #222222;\n        }\n\n        &::-webkit-scrollbar-thumb {\n            background: #333333;\n        }\n\n        &::-webkit-scrollbar-thumb:hover {\n            background: #888888;\n        }\n    }\n}\n\n/////////// layout end ///////////\n\n/////////// global start ///////////\n\n.text-link {\n    color: #007bff;\n    cursor: pointer;\n}\n\n.hover\\:text-primary {\n    &:hover {\n        color: var(--color-primary);\n    }\n}\n\n.bg-default {\n    background-color: var(--color-background);\n}\n\n.bg-primary {\n    background-color: var(--color-primary);\n}\n\n.text-default {\n    color: var(--color-text);\n}\n\n.text-primary {\n    color: var(--color-primary);\n}\n\n.border-default {\n    border-color: var(--color-border);\n}\n\n.plugin-logo-filter {\n    filter: drop-shadow(0 0 1px #FFF);\n}\n\n.debug {\n    border: 1px solid red;\n}\n\n/////////// global end ///////////\n\n\n/////////// arco start ///////////\n.arco-btn, .arco-input-wrapper, .arco-textarea-wrapper, .arco-input-tag, .arco-radio-group-button, .arco-alert {\n    border-radius: 0.5rem;\n}\n\n.arco-tabs-tab-title {\n    &:before {\n        border-radius: 0.5rem !important;\n    }\n}\n\n.arco-modal-confirm {\n    .arco-modal-header {\n        border-bottom: none;\n    }\n\n    .arco-modal-footer {\n        border-top: none;\n    }\n}\n\n.arco-select {\n    border-radius: 0.5rem;\n}\n\n.arco-slider {\n    margin-bottom: 0 !important;\n\n    .arco-slider-mark {\n        font-size: 10px !important;\n    }\n}\n\n.arco-checkbox {\n    padding-left: 0;\n}\n\n.arco-radio-button.arco-radio-checked {\n    border-radius: 0.5rem;\n}\n\n.arco-select-view-multiple {\n    border-radius: 0.5rem;\n}\n\n[data-theme=\"dark\"] {\n    .arco-radio-group-button {\n        .arco-radio-button {\n            color: #CCC;\n        }\n    }\n}\n/////////// arco end ///////////\n\n"
  },
  {
    "path": "src/task/index.ts",
    "content": "import { useTaskStore } from \"../store/modules/task\";\nimport { nextTick } from \"vue\";\n\nconst taskStore = useTaskStore();\n\nexport const TaskManager = {\n    init() {\n        // taskStore.register('SoundTts', SoundTts)\n        // taskStore.register('SoundClone', SoundClone)\n        // taskStore.register('VideoGen', VideoGen)\n        nextTick(async () => {\n            //     await SoundTts.restore?.()\n            //     await SoundClone.restore?.()\n            //     await VideoGen.restore?.()\n        }).then();\n        // taskStore.register('TestSync', TestSync)\n        // taskStore.register('TestAsync', TestAsync)\n        // setInterval(async () => {\n        //     // await taskStore.dispatch('TestSync', StringUtil.random())\n        //     await taskStore.dispatch('TestAsync', StringUtil.random(), {\n        //         'a': 1,\n        //     }, {\n        //         timeout: 3 * 1000,\n        //     })\n        // }, 10 * 1000)\n    },\n    count() {\n        return taskStore.records.length;\n    },\n};\n"
  },
  {
    "path": "src/types/Manager.ts",
    "content": "import {\n    HotkeyKeyItem,\n    HotkeyKeySimpleItem,\n} from \"../../electron/mapi/keys/type\";\n\nexport type ConfigRecord = {\n    mainTrigger: HotkeyKeyItem;\n    detachWindowTrigger: HotkeyKeyItem;\n    fastPanelTrigger: HotkeyKeySimpleItem;\n};\n\nexport type PluginConfig = {\n    autoDetach: boolean;\n    zoom: number;\n};\n\nexport enum PluginType {\n    SYSTEM = \"system\",\n    STORE = \"store\",\n    ZIP = \"zip\",\n    DIR = \"dir\",\n}\n\nexport enum PluginEnv {\n    DEV = \"dev\",\n    PROD = \"prod\",\n}\n\nexport type PluginPermissionType = \"ClipboardManage\" | \"Api\" | \"File\" | never;\n\nexport type PluginRecord = {\n    // 以下配置信息和原始的 config.json 一致，未经过处理\n    name: string;\n    title: string;\n    version: string;\n    logo: string;\n    main: string;\n    mainView?: string;\n    actions: ActionRecord[];\n    mcp?: {\n        tools?: MCPToolsRecord[];\n    };\n    description?: string;\n    preload?: string;\n    platform?: PlatformType[];\n    versionRequire?: string;\n    editionRequire?: EditionType[];\n    author?: string;\n    homepage?: string;\n    setting?: {\n        autoDetach?: boolean;\n        detachPosition?:\n            | \"center\"\n            | \"left-top\"\n            | \"right-top\"\n            | \"left-bottom\"\n            | \"right-bottom\";\n        detachAlwaysOnTop?: boolean;\n        width?: string;\n        height?: string;\n        singleton?: boolean;\n        zoom?: number;\n        darkModeSupport?: boolean;\n        httpEntry?: boolean;\n        remoteWebCacheEnable?: boolean;\n        moreMenu?: {\n            name: string;\n            title: string;\n        }[];\n        preloadBase?: string;\n        nodeIntegration?: boolean;\n    };\n    permissions?: PluginPermissionType[];\n    development?: {\n        env?: \"dev\" | \"prod\";\n        main?: string;\n        mainView?: string;\n        showDevTools?: boolean;\n        showCodeDevTools?: boolean;\n        keepCodeDevTools?: boolean;\n        showViewDevTools?: boolean;\n    };\n\n    type?: PluginType;\n    env?: PluginEnv;\n    runtime?: {\n        // 插件运行的根目录\n        root?: string | null;\n        // 配置信息\n        config?: PluginConfig;\n        // 远程Web信息\n        remoteWeb?: {\n            userAgent?: string;\n            urlMap?: Record<string, string>;\n            types?: string[];\n            domains?: string[];\n            blocks?: string[];\n        };\n    };\n};\n\nexport type PluginState = {\n    value: string;\n    placeholder: string;\n    isVisible: boolean;\n};\n\nexport type ActionMatch =\n    | ActionMatchText\n    | ActionMatchKey\n    | ActionMatchRegex\n    | ActionMatchFile\n    | ActionMatchImage\n    | ActionMatchWindow\n    | ActionMatchEditor;\n\nexport enum ActionMatchTypeEnum {\n    TEXT = \"text\",\n    KEY = \"key\",\n    REGEX = \"regex\",\n    IMAGE = \"image\",\n    FILE = \"file\",\n    WINDOW = \"window\",\n    EDITOR = \"editor\",\n}\n\nexport type ActionMatchBase = {\n    type: ActionMatchTypeEnum;\n    name?: string;\n};\n\nexport type ActionMatchText = ActionMatchBase & {\n    text: string;\n    minLength: number;\n    maxLength: number;\n};\n\nexport type ActionMatchKey = ActionMatchBase & {\n    key: string;\n};\n\nexport type ActionMatchRegex = ActionMatchBase & {\n    regex: string;\n    title: string;\n    minLength: number;\n    maxLength: number;\n};\n\nexport type ActionMatchFile = ActionMatchBase & {\n    title: string;\n    minCount: number;\n    maxCount: number;\n    filterFileType: \"file\" | \"directory\";\n    filterExtensions: string[];\n};\n\nexport type ActionMatchImage = ActionMatchBase & {\n    title: string;\n};\n\nexport type ActionMatchWindow = ActionMatchBase & {\n    nameRegex: string;\n    titleRegex: string;\n    attrRegex: Record<string, string>;\n};\n\nexport type ActionMatchEditor = ActionMatchBase & {\n    extensions: string[];\n    fadTypes: string[];\n};\n\nexport type SelectedContent = {\n    type: \"file\" | \"image\" | \"text\";\n    files?: FileItem[];\n    image?: string;\n    text?: string;\n};\n\nexport type ActiveWindow = {\n    name: string;\n    title: string;\n    attr: Record<string, string>;\n    raw?: any;\n};\n\nexport type ClipboardDataType = {\n    type: \"file\" | \"image\" | \"text\";\n    files?: FileItem[];\n    image?: string;\n    text?: string;\n};\n\nexport type ClipboardHistoryRecord = {\n    type: \"file\" | \"image\" | \"text\";\n    timestamp: number;\n    files?: FileItem[];\n    image?: string;\n    text?: string;\n};\n\nexport type ActionRecord = {\n    fullName?: string;\n    pluginName?: string;\n    name: string;\n    title: string;\n    matches: ActionMatch[];\n    pluginType?: PluginType;\n    platform?: PlatformType[];\n    icon?: string;\n    trackHistory?: boolean;\n    data?: {\n        // type = command\n        command?: string;\n        // type = view\n        showFastPanel?: boolean;\n        showMainPanel?: boolean;\n    };\n\n    type?: ActionTypeEnum;\n    runtime?: {\n        searchScore?: number;\n        searchTitleMatched?: string;\n        match?: ActionMatch | null;\n        requestId?: string | null;\n        view?: {\n            nodeIntegration?: boolean;\n            preloadBase?: string;\n            mainView?: string;\n            showViewDevTools?: boolean;\n            heightView?: number;\n        };\n        matchFiles?: FileItem[];\n        isPined?: boolean;\n        windowId?: number;\n        windowIndex?: number;\n        windowCount?: number;\n    };\n};\n\nexport type MCPToolsRecord = {\n    name: string;\n    description: string;\n    inputSchema: {\n        type: \"object\";\n        properties: Record<\n            string,\n            { type: string; description?: string; default?: any }\n        >;\n        required?: keyof MCPToolsRecord[\"inputSchema\"][\"properties\"][];\n    };\n};\n\nexport type PluginActionRecord = {\n    pluginName: string;\n    actionName: string;\n};\n\nexport type ActionTypeCodeData = {\n    actionName: string;\n};\n\nexport enum ActionTypeEnum {\n    COMMAND = \"command\",\n    WEB = \"web\",\n    CODE = \"code\",\n    BACKEND = \"backend\",\n    VIEW = \"view\",\n}\n\nexport type FilePluginRecord = {\n    icon: string;\n    title: string;\n    path: string;\n};\n\nexport type LaunchRecord = {\n    type: \"plugin\" | \"custom\";\n    pluginName: string;\n    name: string;\n    hotkey: HotkeyKeyItem;\n    keyword: string;\n};\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"unplugin-icons/types/vue\" />\nimport type { Dialog } from \"./lib/dialog\";\nimport type { Router } from \"vue-router\";\n\ndeclare module \"*.vue\" {\n    import type { DefineComponent } from \"vue\";\n    const component: DefineComponent<{}, {}, any>;\n    export default component;\n}\n\ndeclare module \"@vue/runtime-core\" {\n    interface ComponentCustomProperties {\n        $router: Router;\n        $dialog: Dialog;\n        $t: typeof import(\"vue-i18n\").GlobalTranslate;\n    }\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n    content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx,vue}\"],\n    darkMode: [\"selector\", '[data-theme=\"dark\"]'],\n    theme: {\n        extend: {},\n    },\n    plugins: [],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ESNext\",\n        \"useDefineForClassFields\": true,\n        \"module\": \"ESNext\",\n        \"moduleResolution\": \"Node\",\n        \"strict\": true,\n        \"jsx\": \"preserve\",\n        \"resolveJsonModule\": true,\n        \"isolatedModules\": true,\n        \"esModuleInterop\": true,\n        \"lib\": [\"ESNext\", \"DOM\"],\n        \"skipLibCheck\": true,\n        \"noEmit\": true,\n        \"noImplicitAny\": false,\n        \"allowJs\": true\n    },\n    \"include\": [\"src\", \"sdk/focusany.d.ts\"],\n    \"references\": [\n        {\n            \"path\": \"./tsconfig.node.json\"\n        }\n    ]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n    \"compilerOptions\": {\n        \"composite\": true,\n        \"module\": \"ESNext\",\n        \"moduleResolution\": \"Node\",\n        \"resolveJsonModule\": true,\n        \"allowSyntheticDefaultImports\": true\n    },\n    \"include\": [\"vite.config.ts\", \"package.json\", \"electron\", \"sdk\", \"src/config.ts\", \"src/types\"]\n}\n"
  },
  {
    "path": "vite.config.flat.txt",
    "content": "import { rmSync } from 'node:fs'\nimport { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport electron from 'vite-plugin-electron'\nimport renderer from 'vite-plugin-electron-renderer'\nimport pkg from './package.json'\n\n// https://vitejs.dev/config/\nexport default defineConfig(({ command }) => {\n  rmSync('dist-electron', { recursive: true, force: true })\n\n  const isServe = command === 'serve'\n  const isBuild = command === 'build'\n  const sourcemap = isServe || !!process.env.VSCODE_DEBUG\n\n  return {\n    plugins: [\n      vue(),\n      electron([\n        {\n          // Main process entry file of the Electron App.\n          entry: 'electron/main/index.ts',\n          onstart({ startup }) {\n            if (process.env.VSCODE_DEBUG) {\n              console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App')\n            } else {\n              startup()\n            }\n          },\n          vite: {\n            build: {\n              sourcemap,\n              minify: isBuild,\n              outDir: 'dist-electron/main',\n              rollupOptions: {\n                // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, \n                // we can use `external` to exclude them to ensure they work correctly.\n                // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built.\n                // Of course, this is not absolute, just this way is relatively simple. :)\n                external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),\n              },\n            },\n          },\n        },\n        {\n          entry: 'electron/preload/index.ts',\n          onstart({ reload }) {\n            // Notify the Renderer process to reload the page when the Preload scripts build is complete, \n            // instead of restarting the entire Electron App.\n            reload()\n          },\n          vite: {\n            build: {\n              sourcemap: sourcemap ? 'inline' : undefined, // #332\n              minify: isBuild,\n              outDir: 'dist-electron/preload',\n              rollupOptions: {\n                external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),\n              },\n            },\n          },\n        }\n      ]),\n      // Use Node.js API in the Renderer process\n      renderer(),\n    ],\n    server: process.env.VSCODE_DEBUG && (() => {\n      const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)\n      return {\n        host: url.hostname,\n        port: +url.port,\n      }\n    })(),\n    clearScreen: false,\n  }\n})\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import fs from \"node:fs\";\nimport {defineConfig} from \"vite\";\nimport vue from \"@vitejs/plugin-vue\";\nimport Icons from \"unplugin-icons/vite\";\nimport electron from \"vite-plugin-electron\";\nimport renderer from \"vite-plugin-electron-renderer\";\nimport pkg from \"./package.json\";\nimport path from \"node:path\";\nimport {AppConfig} from \"./src/config\";\nimport dayjs from \"dayjs\";\nimport utc from \"dayjs/plugin/utc\";\nimport timezone from \"dayjs/plugin/timezone\";\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\n// https://vitejs.dev/config/\nexport default defineConfig(({command}) => {\n    fs.rmSync(\"dist-electron\", {recursive: true, force: true});\n\n    const isServe = command === \"serve\";\n    const isBuild = command === \"build\";\n    const sourcemap = isServe || !!process.env.VSCODE_DEBUG;\n    const minify = isBuild && !process.env.VSCODE_DEBUG;\n\n    const externalPackages = [\n        ...Object.keys(\"dependencies\" in pkg ? pkg.dependencies : {}),\n        ...Object.keys(\"devDependencies\" in pkg ? pkg.devDependencies : {}),\n        ...Object.keys(\"optionalDependencies\" in pkg ? pkg.optionalDependencies : {}),\n    ];\n\n    return {\n        plugins: [\n            vue({\n                template: {\n                    compilerOptions: {\n                        isCustomElement: tag => {\n                            if ([\"webview\"].includes(tag)) {\n                                return true;\n                            }\n                            return false;\n                        },\n                    },\n                },\n            }),\n            Icons({\n                compiler: \"vue3\",\n                autoInstall: false,\n            }),\n            {\n                name: \"add-build-time\",\n                generateBundle() {\n                    const buildId = dayjs().tz(\"Asia/Shanghai\").format(\"YYYYMMDDHHmmss\");\n                    this.emitFile({\n                        type: \"asset\",\n                        fileName: \"build.json\",\n                        source: JSON.stringify(\n                            {\n                                buildId,\n                            },\n                            null,\n                            2\n                        ),\n                    });\n                },\n            },\n            {\n                name: \"process-variables\",\n                closeBundle() {\n                    const files = [\n                        \"index.html\",\n                        \"page/about.html\",\n                        \"page/feedback.html\",\n                        \"page/guide.html\",\n                        \"page/monitor.html\",\n                        \"page/payment.html\",\n                        \"page/setup.html\",\n                        \"page/user.html\",\n                        \"page/log.html\",\n                    ];\n                    files.forEach(f => {\n                        const p = path.resolve(__dirname, \"dist\", f);\n                        if(!fs.existsSync(p)) {\n                            return;\n                        }\n                        let html = fs.readFileSync(p, \"utf-8\");\n                        for (const key in AppConfig) {\n                            html = html.replace(new RegExp(`%${key}%`, \"g\"), AppConfig[key]);\n                        }\n                        fs.writeFileSync(p, html, \"utf-8\");\n                    });\n                },\n            },\n            electron([\n                {\n                    // Shortcut of `build.lib.entry`\n                    entry: \"electron/main/index.ts\",\n                    onstart({startup}) {\n                        if (process.env.VSCODE_DEBUG) {\n                            console.log(/* For `.vscode/.debug.script.mjs` */ \"[startup] Electron App\");\n                        } else {\n                            startup();\n                        }\n                    },\n                    vite: {\n                        build: {\n                            sourcemap,\n                            minify: minify,\n                            outDir: \"dist-electron/main\",\n                            rollupOptions: {\n                                // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons,\n                                // we can use `external` to exclude them to ensure they work correctly.\n                                // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built.\n                                // Of course, this is not absolute, just this way is relatively simple. :)\n                                external: externalPackages,\n                            },\n                        },\n                    },\n                },\n                {\n                    // Shortcut of `build.rollupOptions.input`.\n                    // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.\n                    entry: \"electron/preload/index.ts\",\n                    onstart({reload}) {\n                        // Notify the Renderer process to reload the page when the Preload scripts build is complete,\n                        // instead of restarting the entire Electron App.\n                        reload();\n                    },\n                    vite: {\n                        build: {\n                            target: \"es2015\",\n                            sourcemap: undefined, // #332\n                            minify: minify,\n                            outDir: \"dist-electron/preload\",\n                            lib: {\n                                formats: [\"cjs\"],\n                                fileName: \"index\",\n                            },\n                            rollupOptions: {\n                                external: externalPackages,\n                                output: {\n                                    format: \"cjs\",\n                                    // entryFileNames: '[name].cjs',\n                                    strict: true,\n                                },\n                            },\n                        },\n                    },\n                },\n                {\n                    // Shortcut of `build.rollupOptions.input`.\n                    // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.\n                    entry: \"electron/preload/plugin.ts\",\n                    onstart({reload}) {\n                        // Notify the Renderer process to reload the page when the Preload scripts build is complete,\n                        // instead of restarting the entire Electron App.\n                        reload();\n                    },\n                    vite: {\n                        build: {\n                            target: \"es2015\",\n                            sourcemap: undefined, // #332\n                            minify: minify,\n                            outDir: \"dist-electron/preload-plugin\",\n                            lib: {\n                                formats: [\"cjs\"],\n                                fileName: \"plugin\",\n                            },\n                            rollupOptions: {\n                                external: externalPackages,\n                                output: {\n                                    format: \"cjs\",\n                                    // entryFileNames: '[name].cjs',\n                                    strict: true,\n                                },\n                            },\n                        },\n                    },\n                },\n            ]),\n            renderer(),\n        ],\n        build: {\n            sourcemap: sourcemap,\n            rollupOptions: {\n                input: {\n                    main: path.resolve(__dirname, \"index.html\"),\n                    detachWindow: path.resolve(__dirname, \"page/detachWindow.html\"),\n                    fastPanel: path.resolve(__dirname, \"page/fastPanel.html\"),\n                    // 内置插件\n                    system: path.resolve(__dirname, \"page/system.html\"),\n                    store: path.resolve(__dirname, \"page/store.html\"),\n                    workflow: path.resolve(__dirname, \"page/workflow.html\"),\n                    // 其他页面\n                    about: path.resolve(__dirname, \"page/about.html\"),\n                    feedback: path.resolve(__dirname, \"page/feedback.html\"),\n                    user: path.resolve(__dirname, \"page/user.html\"),\n                    guide: path.resolve(__dirname, \"page/guide.html\"),\n                    setup: path.resolve(__dirname, \"page/setup.html\"),\n                    payment: path.resolve(__dirname, \"page/payment.html\"),\n                    monitor: path.resolve(__dirname, \"page/monitor.html\"),\n                    log: path.resolve(__dirname, \"page/log.html\"),\n                },\n            },\n        },\n        server: {\n            port: 20000,\n        },\n    };\n});\n"
  }
]