Repository: modstart-lib/focusany Branch: main Commit: 8a5b80119501 Files: 368 Total size: 1.5 MB Directory structure: gitextract_d_g1kj1z/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── help_wanted.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── build.yml │ ├── main-build.yml │ └── tag-release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── cli/ │ ├── cmd/ │ │ ├── plugin.go │ │ ├── root.go │ │ └── version.go │ ├── go.mod │ ├── go.sum │ ├── internal/ │ │ ├── client.go │ │ └── config.go │ └── main.go ├── electron/ │ ├── config/ │ │ ├── common.ts │ │ ├── contextMenu.ts │ │ ├── icon.ts │ │ ├── lang.ts │ │ ├── menu.ts │ │ ├── tray.ts │ │ └── window.ts │ ├── declarations/ │ │ ├── electron.d.ts │ │ └── svg.d.ts │ ├── electron-env.d.ts │ ├── lib/ │ │ ├── api.ts │ │ ├── devtools.ts │ │ ├── env-main.ts │ │ ├── env.ts │ │ ├── hooks.ts │ │ ├── permission.ts │ │ ├── pinyin-util.ts │ │ ├── process.ts │ │ └── util.ts │ ├── main/ │ │ ├── fastPanel.ts │ │ └── index.ts │ ├── mapi/ │ │ ├── app/ │ │ │ ├── icons.ts │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ └── position.ts │ │ │ ├── loading.ts │ │ │ ├── main.ts │ │ │ ├── render.ts │ │ │ ├── setup.ts │ │ │ └── toast.ts │ │ ├── config/ │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── db/ │ │ │ ├── db.ts │ │ │ ├── main.ts │ │ │ ├── migration.ts │ │ │ ├── render.ts │ │ │ └── type.d.ts │ │ ├── env.ts │ │ ├── event/ │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── file/ │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── httpserver/ │ │ │ └── main.ts │ │ ├── keys/ │ │ │ ├── main.ts │ │ │ └── type.ts │ │ ├── kvdb/ │ │ │ ├── kvdb.ts │ │ │ ├── main.ts │ │ │ ├── render.ts │ │ │ ├── types.ts │ │ │ ├── version.ts │ │ │ └── webdav.ts │ │ ├── log/ │ │ │ ├── beacon-render.ts │ │ │ ├── beacon.ts │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── main.ts │ │ ├── manager/ │ │ │ ├── automation/ │ │ │ │ └── index.ts │ │ │ ├── backend/ │ │ │ │ └── index.ts │ │ │ ├── clipboard/ │ │ │ │ ├── clipboardFiles.ts │ │ │ │ └── index.ts │ │ │ ├── code/ │ │ │ │ └── index.ts │ │ │ ├── config/ │ │ │ │ └── config.ts │ │ │ ├── editor/ │ │ │ │ └── index.ts │ │ │ ├── hotkey/ │ │ │ │ ├── handle.ts │ │ │ │ ├── index.ts │ │ │ │ └── simulate.ts │ │ │ ├── lib/ │ │ │ │ ├── cache.ts │ │ │ │ └── hooks.ts │ │ │ ├── main.ts │ │ │ ├── manager.ts │ │ │ ├── plugin/ │ │ │ │ ├── colorPicker.ts │ │ │ │ ├── event.ts │ │ │ │ ├── http.ts │ │ │ │ ├── httpMCP.ts │ │ │ │ ├── index.ts │ │ │ │ ├── llm.ts │ │ │ │ ├── log.ts │ │ │ │ ├── permission.ts │ │ │ │ ├── screenCapture.ts │ │ │ │ ├── screenRecord.ts │ │ │ │ └── sdk.ts │ │ │ ├── render.ts │ │ │ ├── storage/ │ │ │ │ └── index.ts │ │ │ ├── system/ │ │ │ │ ├── asset/ │ │ │ │ │ └── icon.ts │ │ │ │ ├── index.ts │ │ │ │ └── plugin/ │ │ │ │ ├── app/ │ │ │ │ │ ├── linux/ │ │ │ │ │ │ ├── icon.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── title.ts │ │ │ │ │ ├── mac/ │ │ │ │ │ │ ├── icon.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── title.ts │ │ │ │ │ ├── type.ts │ │ │ │ │ ├── util/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── win/ │ │ │ │ │ ├── icon.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── title.ts │ │ │ │ ├── app.ts │ │ │ │ ├── file.ts │ │ │ │ ├── store/ │ │ │ │ │ ├── action.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── store.ts │ │ │ │ ├── system/ │ │ │ │ │ └── action.ts │ │ │ │ └── system.ts │ │ │ ├── type.ts │ │ │ └── window/ │ │ │ ├── index.ts │ │ │ └── remoteWeb.ts │ │ ├── misc/ │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── protocol/ │ │ │ └── main.ts │ │ ├── render.ts │ │ ├── statistics/ │ │ │ └── render.ts │ │ ├── storage/ │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── ui/ │ │ │ ├── index.ts │ │ │ └── render.ts │ │ ├── updater/ │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ └── render.ts │ │ ├── user/ │ │ │ ├── main.ts │ │ │ └── render.ts │ │ └── util.ts │ ├── page/ │ │ ├── about.ts │ │ ├── feedback.ts │ │ ├── guide.ts │ │ ├── index.ts │ │ ├── log.ts │ │ ├── monitor.ts │ │ ├── payment.ts │ │ ├── setup.ts │ │ └── user.ts │ ├── preload/ │ │ ├── focusany.ts │ │ ├── index.ts │ │ └── plugin.ts │ └── resources/ │ └── build/ │ ├── entitlements.mac.plist │ ├── logo.icns │ └── logo_1024x1024.psd ├── electron-builder.json5 ├── entitlements.mac.plist ├── index.html ├── package.json ├── page/ │ ├── about.html │ ├── detachWindow.html │ ├── fastPanel.html │ ├── feedback.html │ ├── guide.html │ ├── log.html │ ├── monitor.html │ ├── payment.html │ ├── setup.html │ ├── store.html │ ├── system.html │ ├── user.html │ └── workflow.html ├── postcss.config.js ├── public/ │ ├── iconfont/ │ │ ├── iconfont.css │ │ ├── iconfont.js │ │ └── iconfont.json │ └── static/ │ └── pluginEmpty.html ├── scripts/ │ ├── build_optimize.cjs │ ├── common.cjs │ ├── icon_convert.sh │ ├── init.sh │ └── notarize.cjs ├── sdk/ │ ├── .babelrc │ ├── .github/ │ │ └── workflows/ │ │ └── tag-release.yml │ ├── .gitignore │ ├── .npmignore │ ├── .nvmrc │ ├── README.md │ ├── bin/ │ │ └── command.ts │ ├── config.schema.json │ ├── electron-browser-window.d.ts │ ├── electron.d.ts │ ├── focusany-shim.d.ts │ ├── focusany-shim.ts │ ├── focusany.d.ts │ ├── index.d.ts │ ├── index.ts │ ├── package.json │ ├── shim.html │ ├── tests/ │ │ └── config.json │ └── tsconfig.json ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── types/ │ │ │ └── base.ts │ │ └── user.ts │ ├── app/ │ │ ├── dragWindow.ts │ │ └── locale.ts │ ├── components/ │ │ ├── AppQuitConfirm.vue │ │ ├── PageNav.vue │ │ ├── Setting/ │ │ │ ├── SettingAbout.vue │ │ │ ├── SettingBasic.vue │ │ │ ├── SettingEnv.vue │ │ │ └── components/ │ │ │ └── SettingEnvHubRoot.vue │ │ ├── TextTruncateView.vue │ │ └── common/ │ │ ├── AudioPlayer.vue │ │ ├── CodeViewer.vue │ │ ├── CodeViewerDialog.vue │ │ ├── DataConfigDialogButton.vue │ │ ├── DragPasteContainer.vue │ │ ├── FeedbackTicketButton.vue │ │ ├── FileExt.vue │ │ ├── FileLogViewer.vue │ │ ├── FilesSelector.vue │ │ ├── HtmlViewer.vue │ │ ├── InputInlineEditor.vue │ │ ├── LogViewer.vue │ │ ├── LogViewerDialog.vue │ │ ├── MEmpty.vue │ │ ├── MLoading.vue │ │ ├── PageWebviewStatus.vue │ │ ├── ProUpgrade.vue │ │ ├── SettingItemYesNo.vue │ │ ├── SettingItemYesNoDefault.vue │ │ ├── TaskBizStatus.vue │ │ ├── UpdaterButton.vue │ │ ├── VideoPlayer.vue │ │ ├── WebFileSelectButton.vue │ │ ├── dataConfig.ts │ │ ├── index.ts │ │ └── util.ts │ ├── config.ts │ ├── declarations/ │ │ ├── svg.d.ts │ │ └── type.d.ts │ ├── entry/ │ │ ├── Page.vue │ │ ├── about.ts │ │ ├── detachWindow.ts │ │ ├── fastPanel.ts │ │ ├── feedback.ts │ │ ├── guide.ts │ │ ├── log.ts │ │ ├── monitor.ts │ │ ├── payment.ts │ │ ├── setup.ts │ │ ├── store.ts │ │ ├── system.ts │ │ ├── user.ts │ │ └── workflow.ts │ ├── hooks/ │ │ └── user.ts │ ├── lang/ │ │ ├── en-US.json │ │ ├── index.ts │ │ └── zh-CN.json │ ├── layouts/ │ │ ├── Main.vue │ │ └── Raw.vue │ ├── lib/ │ │ ├── api.ts │ │ ├── audio.ts │ │ ├── components/ │ │ │ └── Prompt.vue │ │ ├── dialog.ts │ │ ├── env.ts │ │ ├── error.ts │ │ ├── event.ts │ │ ├── file.ts │ │ ├── markdown.ts │ │ ├── storage.ts │ │ ├── toggle.ts │ │ ├── ui.ts │ │ └── util.ts │ ├── main.ts │ ├── module/ │ │ └── Model/ │ │ ├── ModelGenerateButton.vue │ │ ├── ModelGenerator.vue │ │ ├── ModelPromptDataConfigButton.vue │ │ ├── ModelSelector.vue │ │ ├── ModelSetting.vue │ │ ├── ModelSettingDialog.vue │ │ ├── components/ │ │ │ ├── ModelAddDialog.vue │ │ │ ├── ModelEditDialog.vue │ │ │ ├── ProviderAddDialog.vue │ │ │ ├── ProviderEditDialog.vue │ │ │ └── ProviderTestDialog.vue │ │ ├── models.ts │ │ ├── provider/ │ │ │ ├── driver/ │ │ │ │ ├── base.ts │ │ │ │ └── openai.ts │ │ │ └── provider.ts │ │ ├── providers.ts │ │ ├── store/ │ │ │ └── model.ts │ │ └── types.ts │ ├── pages/ │ │ ├── DetachWindow/ │ │ │ └── operate.ts │ │ ├── FastPanel/ │ │ │ ├── FastPanelResult.vue │ │ │ ├── FastPanelSearch.vue │ │ │ └── Lib/ │ │ │ └── resultOperate.ts │ │ ├── Home.vue │ │ ├── Main/ │ │ │ ├── Components/ │ │ │ │ ├── ResultActionCodeError.vue │ │ │ │ ├── ResultActionCodeItemList.vue │ │ │ │ ├── ResultActionCodeLoading.vue │ │ │ │ ├── ResultItem.vue │ │ │ │ ├── ResultLoading.vue │ │ │ │ └── ResultWindowItem.vue │ │ │ ├── Lib/ │ │ │ │ ├── entryListener.ts │ │ │ │ ├── mainOperate.ts │ │ │ │ ├── resultOperate.ts │ │ │ │ ├── resultResize.ts │ │ │ │ ├── searchOperate.ts │ │ │ │ └── viewOperate.ts │ │ │ ├── MainResult.vue │ │ │ └── MainSearch.vue │ │ ├── PageAbout.vue │ │ ├── PageDetachWindow.vue │ │ ├── PageFastPanel.vue │ │ ├── PageFeedback.vue │ │ ├── PageGuide.vue │ │ ├── PageLog.vue │ │ ├── PageMonitor.vue │ │ ├── PagePayment.vue │ │ ├── PageSetup.vue │ │ ├── PageStore.vue │ │ ├── PageSystem.vue │ │ ├── PageUser.vue │ │ ├── PageWorkflow.vue │ │ ├── Setting.vue │ │ └── System/ │ │ ├── SystemAbout.vue │ │ ├── SystemAction.vue │ │ ├── SystemData.vue │ │ ├── SystemFile.vue │ │ ├── SystemLaunch.vue │ │ ├── SystemMCP.vue │ │ ├── SystemModel.vue │ │ ├── SystemPlugin.vue │ │ ├── SystemSetting.vue │ │ ├── SystemUser.vue │ │ └── components/ │ │ ├── ActionTypeIcon.vue │ │ ├── HotkeyInput.vue │ │ ├── SystemActionMatchDetailDialog.vue │ │ ├── SystemDataBackup/ │ │ │ ├── WebDavManage.vue │ │ │ └── WebDavManageSettingDialog.vue │ │ ├── SystemDataBackupDialog.vue │ │ ├── SystemDataViewDetailDialog.vue │ │ ├── SystemDataViewDialog.vue │ │ └── type.ts │ ├── router.ts │ ├── store/ │ │ ├── index.ts │ │ └── modules/ │ │ ├── app.ts │ │ ├── manager.ts │ │ ├── setting.ts │ │ ├── task.ts │ │ └── user.ts │ ├── style.less │ ├── task/ │ │ └── index.ts │ ├── types/ │ │ └── Manager.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.flat.txt └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml, yaml}] indent_size = 4 [*.{less, css}] indent_size = 4 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 🐞 Bug report about: Create a report to help us improve title: "[Bug] the title of bug report" labels: bug assignees: '' --- #### Describe the bug ================================================ FILE: .github/ISSUE_TEMPLATE/help_wanted.md ================================================ --- name: 🥺 Help wanted about: Confuse about the use of electron-vue-vite title: "[Help] the title of help wanted report" labels: help wanted assignees: '' --- #### Describe the problem you confuse ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description ### What is the purpose of this pull request? - [ ] Bug fix - [ ] New Feature - [ ] Documentation update - [ ] Other ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: tags: - v*.*.* branches: - main workflow_dispatch: jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest arch: [ arm64, amd64 ] - os: macos-latest arch: [ arm64, amd64 ] - os: windows-latest arch: [ arm64, amd64 ] steps: - name: Checkout Code uses: actions/checkout@v4 - name: Git clone repo if: runner.os == 'Linux' || runner.os == 'macOS' env: GIT_USER: ${{ secrets.GIT_USER }} GIT_PASS: ${{ secrets.GIT_PASS }} GIT_REPO_BASE: ${{ secrets.GIT_REPO_BASE }} GIT_HOST: ${{ secrets.GIT_HOST }} run: | git clone -b main "https://${GIT_USER}:${GIT_PASS}@${GIT_HOST}/${GIT_REPO_BASE}/focusany-pro.git" code - name: Git clone repo if: runner.os == 'Windows' env: GIT_USER: ${{ secrets.GIT_USER }} GIT_PASS: ${{ secrets.GIT_PASS }} GIT_REPO_BASE: ${{ secrets.GIT_REPO_BASE }} GIT_HOST: ${{ secrets.GIT_HOST }} shell: pwsh run: | git clone -b main "https://${env:GIT_USER}:${env:GIT_PASS}@${env:GIT_HOST}/${env:GIT_REPO_BASE}/focusany-pro.git" code - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Build Prepare (Linux) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y build-essential pkg-config - name: Build Prepare (macOS) if: runner.os == 'macOS' run: | brew install python-setuptools - name: Cert Prepare (macOS) if: runner.os == 'macOS' env: MACOS_CERTIFICATE: ${{ secrets.CORP_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.CORP_MACOS_CERTIFICATE_PASSWORD }} run: | echo "find-identity" security find-identity -p codesigning echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign security list-keychains -s build.keychain security set-keychain-settings -t 3600 -u build.keychain security unlock-keychain -p "" build.keychain echo "find-identity" security find-identity -v -p codesigning build.keychain echo "find-identity" security find-identity -p codesigning echo "set-key-partition-list" 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 echo "export" security export -k build.keychain -t certs -f x509 -p -o certificate.cer echo "add-trusted-cert" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.cer echo "find-identity" security find-identity -p codesigning - name: Install Dependencies (Linux/macOS) if: runner.os == 'Linux' || runner.os == 'macOS' working-directory: code run: npm install - name: Install Dependencies (Windows) if: runner.os == 'Windows' working-directory: code shell: pwsh run: npm install - name: init ( Windows ) if: runner.os == 'Windows' working-directory: code shell: pwsh run: bash ./scripts/init.sh - name: init ( Linux/osx ) if: runner.os == 'Linux' || runner.os == 'macOS' working-directory: code run: bash ./scripts/init.sh - name: Build Release Files working-directory: code run: | npm run build env: DEBUG: "electron-notarize:*" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Set Build Name ( Linux / macOS ) if: runner.os == 'Linux' || runner.os == 'macOS' run: | DIST_FILE_NAME=${{ runner.os }}-${{ runner.arch }}-v$(date +%Y%m%d_%H%M%S)-${RANDOM} echo ::add-mask::$DIST_FILE_NAME echo DIST_FILE_NAME=$DIST_FILE_NAME >> $GITHUB_ENV - name: Set Build Name ( Windows ) if: runner.os == 'Windows' shell: pwsh run: | $randomNumber = Get-Random -Minimum 10000 -Maximum 99999 $DIST_FILE_NAME = "Windows-X64-v$(Get-Date -Format 'yyyyMMdd_HHmmss')-$randomNumber" Write-Host "::add-mask::$DIST_FILE_NAME" echo "DIST_FILE_NAME=$DIST_FILE_NAME" >> $env:GITHUB_ENV - name: Rename output files (Linux/macOS) if: runner.os == 'Linux' || runner.os == 'macOS' run: | find code/dist-release/ -type f -name 'FocusAnyPro-*' -exec bash -c 'f="{}"; mv "$f" "${f/FocusAnyPro-/FocusAny-}"' \; - name: Upload Sourcemaps if: runner.os == 'Linux' working-directory: code env: GROW_URL: ${{ secrets.GROW_URL }} GROW_APP_NAME: ${{ secrets.GROW_APP_NAME }} GROW_ADMIN_API_TOKEN: ${{ secrets.GROW_ADMIN_API_TOKEN }} run: | BUILD_ID=$(node -p "require('./dist/build.json').buildId") echo "Uploading sourcemaps for buildId=${BUILD_ID} ..." find dist dist-electron/main -name "*.map" | while read f; do echo " uploading $f" RESP=$(curl -s -X POST "${GROW_URL}/api/admin/grow/buildUpload" \ -H "Authorization: Bearer ${GROW_ADMIN_API_TOKEN}" \ -F "appName=${GROW_APP_NAME}" \ -F "buildId=${BUILD_ID}" \ -F "file=@${f}" \ -F "name=${f}") echo " response: ${RESP}" done echo "Sourcemap upload done." - name: Rename output files (Windows) if: runner.os == 'Windows' shell: pwsh run: | Get-ChildItem code\dist-release\FocusAnyPro-* | Rename-Item -NewName { $_.Name -replace 'FocusAnyPro-','FocusAny-' } - name: Upload if: github.event_name == 'workflow_dispatch' uses: modstart/github-oss-action@master with: title: ${{ github.event.head_commit.message }} key-id: ${{ secrets.OSS_2_KEY_ID }} key-secret: ${{ secrets.OSS_2_KEY_SECRET }} region: ${{ secrets.OSS_2_REGION }} bucket: ${{ secrets.OSS_2_BUCKET }} callbackTitle: ✅FocusAny-${{ runner.os }}-打包成功 callback: ${{ secrets.OSS_2_CALLBACK }} assets: | code/dist-release/*.exe:apps/focusany-${{ env.DIST_FILE_NAME }}/ code/dist-release/*.dmg:apps/focusany-${{ env.DIST_FILE_NAME }}/ code/dist-release/*.AppImage:apps/focusany-${{ env.DIST_FILE_NAME }}/ code/dist-release/*.deb:apps/focusany-${{ env.DIST_FILE_NAME }}/ - name: Release Assets if: github.event_name == 'push' uses: softprops/action-gh-release@v2 with: draft: true prerelease: false fail_on_unmatched_files: false overwrite_files: true files: | code/dist-release/*.exe code/dist-release/*.dmg code/dist-release/*.AppImage code/dist-release/*.deb env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} ================================================ FILE: .github/workflows/main-build.yml ================================================ name: MainBuild on: push: branches: - mainx jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest arch: [arm64, amd64] - os: macos-latest arch: [arm64, amd64] - os: windows-latest arch: [arm64, amd64] steps: - name: Checkout Code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Build Prepare (Linux) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y build-essential pkg-config - name: Build Prepare (macOS) if: runner.os == 'macOS' run: | brew install python-setuptools - name: Cert Prepare (macOS) if: runner.os == 'macOS' env: MACOS_CERTIFICATE: ${{ secrets.CORP_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.CORP_MACOS_CERTIFICATE_PASSWORD }} run: | echo "find-identity" security find-identity -p codesigning echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign security list-keychains -s build.keychain security set-keychain-settings -t 3600 -u build.keychain security unlock-keychain -p "" build.keychain echo "find-identity" security find-identity -v -p codesigning build.keychain echo "find-identity" security find-identity -p codesigning echo "set-key-partition-list" 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 echo "export" security export -k build.keychain -t certs -f x509 -p -o certificate.cer echo "add-trusted-cert" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.cer echo "find-identity" security find-identity -p codesigning - name: Install Dependencies (Linux/macOS) if: runner.os == 'Linux' || runner.os == 'macOS' run: | npm install --ignore-scripts rm -rf node_modules/canvas npx electron-builder install-app-deps - name: Install Dependencies (Windows) if: runner.os == 'Windows' shell: pwsh run: | npm install --ignore-scripts if (Test-Path node_modules\canvas) { Remove-Item -Recurse -Force node_modules\canvas } npx electron-builder install-app-deps - name: init ( Windows ) if: runner.os == 'Windows' shell: pwsh run: bash ./scripts/init.sh - name: init ( Linux/osx ) if: runner.os == 'Linux' || runner.os == 'macOS' run: bash ./scripts/init.sh - name: Build Release Files run: npm run build env: DEBUG: "electron-notarize:*" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Set Build Name ( Linux / macOS ) if: runner.os == 'Linux' || runner.os == 'macOS' run: | DIST_FILE_NAME=${{ runner.os }}-${{ runner.arch }}-v$(date +%Y%m%d_%H%M%S)-${RANDOM} echo ::add-mask::$DIST_FILE_NAME echo DIST_FILE_NAME=$DIST_FILE_NAME >> $GITHUB_ENV - name: Set Build Name ( Windows ) if: runner.os == 'Windows' shell: pwsh run: | $randomNumber = Get-Random -Minimum 10000 -Maximum 99999 $DIST_FILE_NAME = "Windows-X64-v$(Get-Date -Format 'yyyyMMdd_HHmmss')-$randomNumber" Write-Host "::add-mask::$DIST_FILE_NAME" echo "DIST_FILE_NAME=$DIST_FILE_NAME" >> $env:GITHUB_ENV - name: Upload uses: modstart/github-oss-action@master with: title: ${{ github.event.head_commit.message }} key-id: ${{ secrets.OSS_2_KEY_ID }} key-secret: ${{ secrets.OSS_2_KEY_SECRET }} region: ${{ secrets.OSS_2_REGION }} bucket: ${{ secrets.OSS_2_BUCKET }} callbackUrlSign: ${{ secrets.OSS_2_CALLBACK_URL_SIGN }} callback: ${{ secrets.OSS_2_CALLBACK }} assets: | dist-release/*.exe:apps/focusany-${{ env.DIST_FILE_NAME }}/ dist-release/*.dmg:apps/focusany-${{ env.DIST_FILE_NAME }}/ dist-release/*.AppImage:apps/focusany-${{ env.DIST_FILE_NAME }}/ dist-release/*.deb:apps/focusany-${{ env.DIST_FILE_NAME }}/ - name: Upload Artifact Windows if: runner.os == 'Windows' uses: actions/upload-artifact@v4 with: name: windows-artifact path: | dist-release/*.exe - name: Upload Artifact Macos if: runner.os == 'macOS' uses: actions/upload-artifact@v4 with: name: macos-artifact path: | dist-release/*.dmg - name: Upload Artifact Linux if: runner.os == 'Linux' uses: actions/upload-artifact@v4 with: name: linux-artifact path: | dist-release/*.AppImage dist-release/*.deb ================================================ FILE: .github/workflows/tag-release.yml ================================================ name: TagRelease on: push: tags: - v*.*.* jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest arch: [arm64, amd64] - os: macos-latest arch: [arm64, amd64] - os: windows-latest arch: [arm64, amd64] steps: - name: Checkout Code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Build Prepare (Linux) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y build-essential pkg-config - name: Build Prepare (macOS) if: runner.os == 'macOS' run: | brew install python-setuptools - name: Cert Prepare (macOS) if: runner.os == 'macOS' env: MACOS_CERTIFICATE: ${{ secrets.CORP_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.CORP_MACOS_CERTIFICATE_PASSWORD }} run: | echo "find-identity" security find-identity -p codesigning echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign security list-keychains -s build.keychain security set-keychain-settings -t 3600 -u build.keychain security unlock-keychain -p "" build.keychain echo "find-identity" security find-identity -v -p codesigning build.keychain echo "find-identity" security find-identity -p codesigning echo "set-key-partition-list" 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 echo "export" security export -k build.keychain -t certs -f x509 -p -o certificate.cer echo "add-trusted-cert" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.cer echo "find-identity" security find-identity -p codesigning - name: Install Dependencies (Linux/macOS) if: runner.os == 'Linux' || runner.os == 'macOS' run: | npm install --ignore-scripts rm -rf node_modules/canvas npx electron-builder install-app-deps - name: Install Dependencies (Windows) if: runner.os == 'Windows' shell: pwsh run: | npm install --ignore-scripts if (Test-Path node_modules\canvas) { Remove-Item -Recurse -Force node_modules\canvas } npx electron-builder install-app-deps - name: init ( Windows ) if: runner.os == 'Windows' shell: pwsh run: bash ./scripts/init.sh - name: init ( Linux/osx ) if: runner.os == 'Linux' || runner.os == 'macOS' run: bash ./scripts/init.sh - name: Build Release Files run: npm run build env: DEBUG: "electron-notarize:*" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Set Build Name ( Linux / macOS ) if: runner.os == 'Linux' || runner.os == 'macOS' run: | DIST_FILE_NAME=${{ runner.os }}-${{ runner.arch }}-v$(date +%Y%m%d_%H%M%S)-${RANDOM} echo ::add-mask::$DIST_FILE_NAME echo DIST_FILE_NAME=$DIST_FILE_NAME >> $GITHUB_ENV - name: Set Build Name ( Windows ) if: runner.os == 'Windows' shell: pwsh run: | $randomNumber = Get-Random -Minimum 10000 -Maximum 99999 $DIST_FILE_NAME = "Windows-X64-v$(Get-Date -Format 'yyyyMMdd_HHmmss')-$randomNumber" Write-Host "::add-mask::$DIST_FILE_NAME" echo "DIST_FILE_NAME=$DIST_FILE_NAME" >> $env:GITHUB_ENV ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules /dist /dist-ssr /dist-electron /dist-release *.local # Editor directories and files .vscode/.debug.env .idea/ .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? # lockfile pnpm-lock.yaml yarn.lock database.db /focusany-plugin-* /data /data-* .vscode .github/copilot-instructions.md .vscode .github/copilot-instructions.md .vscode ================================================ FILE: .npmrc ================================================ # For electron-builder # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 shamefully-hoist=true # For China 🇨🇳 developers electron_mirror=https://npmmirror.com/mirrors/electron/ electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ ================================================ FILE: .nvmrc ================================================ 20 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 2. 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. 3. 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. 4. 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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You 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 If 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. You 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. 5. 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. 6. 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. 7. 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. 8. 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. 9. 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. END OF TERMS AND CONDITIONS ================================================ FILE: Makefile ================================================ # make test → 完整测试套件(构建验证) # make test biz → 直接运行全部 test/biz/*.test.ts(需已启动 Electron) # make test ui → 直接运行全部 test/ui/*.test.ts(需已启动 Electron) # make test biz xxx → 单独运行 test/biz/xxx.test.ts # make test ui xxx → 单独运行 test/ui/xxx.test.ts _TEST_TYPE := $(word 2,$(MAKECMDGOALS)) _TEST_NAME := $(word 3,$(MAKECMDGOALS)) .PHONY: test biz ui dev-seed dev publish build_and_install build-cli test: @if [ "$(_TEST_TYPE)" = "biz" ] && [ -n "$(_TEST_NAME)" ]; then \ npx tsx test/biz/$(_TEST_NAME).test.ts; \ elif [ "$(_TEST_TYPE)" = "ui" ] && [ -n "$(_TEST_NAME)" ]; then \ npx tsx test/ui/$(_TEST_NAME).test.ts; \ elif [ "$(_TEST_TYPE)" = "biz" ]; then \ failed=0; \ for f in test/biz/*.test.ts; do [ -f "$$f" ] || continue; npx tsx "$$f" || failed=1; done; \ exit $$failed; \ elif [ "$(_TEST_TYPE)" = "ui" ]; then \ failed=0; \ for f in test/ui/*.test.ts; do [ -f "$$f" ] || continue; npx tsx "$$f" || failed=1; done; \ exit $$failed; \ else \ npm run build:preview 2>&1 | tail -20; \ fi biz ui: @: dev-seed: npx tsx test/dev-seed.ts dev: npm run dev:mac publish: ss-publish publish ../focusany cd ../focusany && make test build_and_install: rm -rfv dist-release/*.dmg rm -rfv dist-release/*.blockmap rm -rfv dist-release/*.yml rm -rfv dist-release/*.yaml rm -rfv dist-release/*.zip npm run build:mac-arm rm -rfv /Applications/FocusAny.app cp -a ./dist-release/mac-arm64/FocusAny.app /Applications build-cli: @VERSION=$$(node -p "require('./package.json').version") && \ mkdir -p dist-cli && \ echo "Building CLI version $$VERSION ..." && \ cd cli && \ GOOS=darwin GOARCH=amd64 go build -ldflags="-X main.Version=$$VERSION" -o ../dist-cli/focusany-darwin-x64 . && \ GOOS=darwin GOARCH=arm64 go build -ldflags="-X main.Version=$$VERSION" -o ../dist-cli/focusany-darwin-arm64 . && \ GOOS=linux GOARCH=amd64 go build -ldflags="-X main.Version=$$VERSION" -o ../dist-cli/focusany-linux-x64 . && \ GOOS=linux GOARCH=arm64 go build -ldflags="-X main.Version=$$VERSION" -o ../dist-cli/focusany-linux-arm64 . && \ GOOS=windows GOARCH=amd64 go build -ldflags="-X main.Version=$$VERSION" -o ../dist-cli/focusany-win-x64.exe . && \ GOOS=windows GOARCH=arm64 go build -ldflags="-X main.Version=$$VERSION" -o ../dist-cli/focusany-win-arm64.exe . && \ echo "CLI binaries built in dist-cli/" # 吸收 `make test biz/ui ` 中任意测试名称,防止 "No rule to make target" 报错 .DEFAULT: @: ================================================ FILE: README.md ================================================ # 🎯 FocusAny - 智能AI办公助理
FocusAny 主界面
Framework Website GitHub Stars Gitee Stars GitCode Stars License
## ✨ 项目简介 `FocusAny` 是一个强大的智能AI办公助理,专为提升工作效率而设计。它支持市场插件和本地插件的一键启动,让你能够快速扩展功能,打造个性化的办公环境。 🚀 **快速启动** | 🔧 **插件扩展** | 🎨 **现代化界面** | 🌙 **暗黑模式支持** ## 📋 功能特性 - ⚙️ **功能设置**:自定义呼出快捷键,开机自启动 - 🛠️ **插件管理**:一键安装、卸载、启用/禁用插件 - 🎯 **动作管理**:内置和插件动作快速预览和管理 - 📁 **文件快速启动**:瞬间定位目标文件 - ⌨️ **快捷键启动**:全局快捷键快速启动应用 - 💾 **数据中心**:文件导出同步、WebDAV 文件同步 - 🌙 **暗黑模式**:护眼的暗黑主题界面 ## 🔌 插件生态 FocusAny 拥有丰富的插件生态系统,支持各种办公场景: ### 插件市场概览
🎪 插件市场
插件市场
📝 Markdown插件 🛠️ Ctool程序员工具箱
Markdown插件 Ctool工具箱
🌐 翻译插件 📋 剪切板插件
翻译插件 剪切板插件
🧠 脑图编辑器 📊 mxGraph编辑器
脑图编辑器 mxGraph编辑器
🎨 tldraw白板 ✏️ Excalidraw白板
tldraw白板 Excalidraw白板
🔐 密码管理器 🖼️ 图片美化
密码管理器 图片美化
🔢 OTP两步验证 📸 截图与贴图
OTP验证 截图工具
💡 **持续扩展**:FocusAny 正在不断添加更多插件,让你通过插件的方式实现无限可能的功能扩展! ## 🚀 快速开始 ### 📦 安装使用 访问 [FocusAny 官网](https://focusany.com) 下载对应系统的安装包,一键安装即可开始使用! ### 🛠️ 本地开发 > ⚠️ 仅在 Node.js 20 环境下测试通过 #### 环境准备 **Ubuntu/Debian:** ```bash sudo apt install -y make gcc g++ python3 ``` **Windows:** - 安装 Visual Studio 2019,并选择 "Desktop Development with C++" 组件 **macOS:** - 安装 Python 3 #### 开发命令 ```bash # 安装项目依赖 npm install # 启动开发模式 npm run dev # 构建生产版本 npm run build ``` ## 🏗️ 技术栈
Electron Vue.js TypeScript Node.js
## 📚 目录结构 ``` focusany/ ├── electron/ # Electron 主进程代码 ├── src/ # Vue.js 前端源码 ├── public/ # 静态资源 ├── scripts/ # 构建脚本 ├── screenshots/ # 截图资源 └── dist-release/ # 构建输出 ``` ## 🤝 社区交流 > 添加好友请备注 "FocusAny"
💬 微信交流群 🗣️ QQ交流群
微信群 QQ群
## 📄 许可证 本项目采用 [Apache-2.0](LICENSE) 许可证开源。 ---

⭐ 如果这个项目对你有帮助,请给我们一个 Star!

💝 感谢所有贡献者和用户的支持

================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions We strongly recommend always using the latest version to benefit from the latest security updates. ## Reporting a Vulnerability We take the security of our software very seriously. If you discover a security vulnerability, please follow these guidelines: ### How to Report Please **DO NOT** create a public issue for security vulnerabilities. Instead, report security vulnerabilities by emailing: 📧 **Email**: `modstart@163.com` ### What to Include When reporting a security vulnerability, please include: 1. **Description**: A clear description of the vulnerability 2. **Steps to Reproduce**: Detailed steps to reproduce the issue 3. **Impact Assessment**: Your assessment of the potential impact 4. **Affected Versions**: Which versions are affected 5. **Proof of Concept**: If applicable, include a PoC or example exploit 6. **Suggested Fix**: If you have ideas on how to fix it (optional) ### Response Process - **Initial Response**: We aim to respond within 48 hours - **Status Updates**: We will keep you informed about the progress - **Disclosure Coordination**: We will coordinate with you on the disclosure timeline - **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous) ### Responsible Disclosure We ask that you: - Give us reasonable time to fix the vulnerability before public disclosure - Avoid exploiting the vulnerability beyond what is necessary to demonstrate it - Do not access, modify, or delete data belonging to others - Do not perform actions that could harm the availability of our services ## Security Updates Security updates will be announced through: - GitHub Releases - Project Documentation - Email notification to users who have reported issues ## Acknowledgments We appreciate the security research community and welcome responsible disclosure of security vulnerabilities. ================================================ FILE: cli/cmd/plugin.go ================================================ package cmd import ( "focusany-cli/internal" "github.com/spf13/cobra" ) var pluginCmd = &cobra.Command{ Use: "plugin", Short: "Manage plugins", } var pluginListCmd = &cobra.Command{ Use: "list", Short: "List all installed plugins", RunE: func(cmd *cobra.Command, args []string) error { cfg, err := internal.LoadAuthConfig() if err != nil { return err } result, err := internal.DoRequest(cfg, "GET", "/api/plugin/list", nil) if err != nil { return err } return internal.PrintJSON(result) }, } func init() { pluginCmd.AddCommand(pluginListCmd) } ================================================ FILE: cli/cmd/root.go ================================================ package cmd import ( "os" "github.com/spf13/cobra" ) var appVersion string // Execute sets the version and runs the root command. func Execute(version string) { appVersion = version if err := rootCmd.Execute(); err != nil { os.Exit(1) } } var rootCmd = &cobra.Command{ Use: "focusany", Short: "FocusAny CLI", Long: "FocusAny command-line tool for interacting with the local FocusAny service.", } func init() { rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(pluginCmd) } ================================================ FILE: cli/cmd/version.go ================================================ package cmd import ( "focusany-cli/internal" "github.com/spf13/cobra" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Print version information", RunE: func(cmd *cobra.Command, args []string) error { return internal.PrintJSON(map[string]string{ "version": appVersion, }) }, } ================================================ FILE: cli/go.mod ================================================ module focusany-cli go 1.22 require github.com/spf13/cobra v1.8.0 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect ) ================================================ FILE: cli/go.sum ================================================ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: cli/internal/client.go ================================================ package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) // DoRequest sends an HTTP request to the local FocusAny HTTP server. func DoRequest(cfg *AuthConfig, method string, urlPath string, body any) (map[string]any, error) { var reqBody io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request body: %w", err) } reqBody = bytes.NewReader(b) } url := fmt.Sprintf("http://127.0.0.1:%d%s", cfg.Port, urlPath) req, err := http.NewRequest(method, url, reqBody) if err != nil { return nil, fmt.Errorf("create request: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set("Authorization", "Bearer "+cfg.Token) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w (is FocusAny running?)", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode == http.StatusUnauthorized { return nil, fmt.Errorf("unauthorized: token mismatch, restart FocusAny and try again") } var result map[string]any if err := json.Unmarshal(respBytes, &result); err != nil { return nil, fmt.Errorf("parse response: %w", err) } return result, nil } // PrintJSON outputs a value as indented JSON to stdout. func PrintJSON(v any) error { b, err := json.MarshalIndent(v, "", " ") if err != nil { return err } fmt.Println(string(b)) return nil } ================================================ FILE: cli/internal/config.go ================================================ package internal import ( "encoding/json" "fmt" "os" "path/filepath" "runtime" ) // AuthConfig holds the port and token read from cli-auth.json type AuthConfig struct { Port int `json:"port"` Token string `json:"token"` } // userDataDir returns the Electron userData directory path matching app.getPath('userData') // which uses the app name "focusany". func userDataDir() (string, error) { switch runtime.GOOS { case "darwin": home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, "Library", "Application Support", "focusany"), nil case "windows": appData := os.Getenv("APPDATA") if appData == "" { return "", fmt.Errorf("APPDATA environment variable not set") } return filepath.Join(appData, "focusany"), nil default: // Linux: XDG_CONFIG_HOME or ~/.config configDir := os.Getenv("XDG_CONFIG_HOME") if configDir == "" { home, err := os.UserHomeDir() if err != nil { return "", err } configDir = filepath.Join(home, ".config") } return filepath.Join(configDir, "focusany"), nil } } // LoadAuthConfig reads cli-auth.json from the focusany userData directory. func LoadAuthConfig() (*AuthConfig, error) { dir, err := userDataDir() if err != nil { return nil, fmt.Errorf("cannot determine userData directory: %w", err) } filePath := filepath.Join(dir, "cli-auth.json") data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("cannot read %s: %w (is FocusAny running?)", filePath, err) } var cfg AuthConfig if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("invalid cli-auth.json: %w", err) } if cfg.Port == 0 || cfg.Token == "" { return nil, fmt.Errorf("cli-auth.json is incomplete (port=%d, token empty=%v)", cfg.Port, cfg.Token == "") } return &cfg, nil } ================================================ FILE: cli/main.go ================================================ package main import "focusany-cli/cmd" // Version is injected at build time via ldflags: -X main.Version=x.x.x var Version = "dev" func main() { cmd.Execute(Version) } ================================================ FILE: electron/config/common.ts ================================================ export const CommonConfig = { darkModeEnable: true, dbSystem: "system", dbConfigId: "config", dbDisabledActionMatchId: "disabledActionMatch", dbPinActionId: "pinAction", dbFileId: "file", dbLaunchId: "launch", dbCustomActionId: "customAction", dbHistoryActionId: "historyAction", dbPluginConfigId: "pluginConfig", dbPluginIdPrefix: "plugin", dbPluginStorageIdPrefix: "storage", }; ================================================ FILE: electron/config/contextMenu.ts ================================================ import contextMenu from "electron-context-menu"; const init = () => { contextMenu({ showSaveImageAs: false, showCopyLink: false, showCopyImage: false, showSelectAll: false, showInspectElement: false, showSearchWithGoogle: false, showLookUpSelection: false, }); }; export const ConfigContextMenu = { init, }; ================================================ FILE: electron/config/icon.ts ================================================ import { buildResolve, extraResolve } from "../lib/env"; export const logoPath = buildResolve("logo.png"); export const icoLogoPath = buildResolve("logo.ico"); export const icnsLogoPath = buildResolve("logo.icns"); export const trayPath = process.platform === "darwin" ? extraResolve("osx/tray/iconTemplate.png") : extraResolve("common/tray/icon.png"); ================================================ FILE: electron/config/lang.ts ================================================ import enUS from "./../../src/lang/en-US.json"; import zhCN from "./../../src/lang/zh-CN.json"; import { isDev } from "../lib/env"; import { ConfigMain } from "../mapi/config/main"; export const defaultLocale = "zh-CN"; let locale = defaultLocale; export const langMessageList = [ { name: "en-US", label: "English", messages: enUS, }, { name: "zh-CN", label: "简体中文", messages: zhCN, }, ]; const buildMessages = (): any => { let messages = {}; for (let m of langMessageList) { messages[m.name] = m.messages; } return messages; }; let messages = buildMessages(); export const t = (text: string, param: object | null = null) => { if (messages[locale]) { if (messages[locale][text]) { if (param) { return messages[locale][text].replace( /\{(\w+)\}/g, function (match, key) { return key in param ? param[key] : match; }, ); } return messages[locale][text]; } } if (param) { return text.replace(/\{(\w+)\}/g, function (match, key) { return key in param ? param[key] : match; }); } return text; }; const readyAsync = async () => { locale = await ConfigMain.get("lang", defaultLocale); }; const getLocale = () => { return locale; }; export const ConfigLang = { readyAsync, getLocale, }; ================================================ FILE: electron/config/menu.ts ================================================ import { app, Menu } from "electron"; import { isDev, isMac } from "../lib/env"; import { t } from "./lang"; let contextMenu: Electron.Menu; const ready = () => { const menuTemplate: Electron.MenuItemConstructorOptions[] = []; if (isMac) { menuTemplate.push({ label: app.name, submenu: [ { label: `${t("menu.about")}${app.name}`, role: "about" }, { type: "separator" }, // { // label: t("设置"), // click: () => { // createSettingWindow(); // }, // accelerator: "CmdOrCtrl+,", // }, // {type: "separator"}, { label: t("menu.services"), role: "services" }, { type: "separator" }, { label: `${t("menu.hide")} ${app.name}`, role: "hide" }, { label: t("menu.hideOthers"), role: "hideOthers" }, { label: t("menu.showAll"), role: "unhide" }, { type: "separator" }, { label: t("menu.quit"), role: "quit" }, ], }); } menuTemplate.push({ label: t("menu.edit"), submenu: [ { label: t("menu.undo"), accelerator: "CmdOrCtrl+Z", role: "undo" }, { label: t("menu.redo"), accelerator: "Shift+CmdOrCtrl+Z", role: "redo", }, { type: "separator" }, { label: t("menu.cut"), accelerator: "CmdOrCtrl+X", role: "cut" }, { label: t("menu.copy"), accelerator: "CmdOrCtrl+C", role: "copy" }, { label: t("menu.paste"), accelerator: "CmdOrCtrl+V", role: "paste", }, { label: t("menu.selectAll"), accelerator: "CmdOrCtrl+A", role: "selectAll", }, ], }); if (isDev) { menuTemplate.push({ label: t("menu.view"), submenu: [ { label: t("menu.reload"), role: "reload" }, { label: t("menu.forceReload"), role: "forceReload" }, { label: t("menu.devTools"), role: "toggleDevTools" }, { type: "separator" }, { label: t("menu.actualSize"), role: "resetZoom", accelerator: "", }, { label: t("menu.zoomIn"), role: "zoomIn" }, { label: t("menu.zoomOut"), role: "zoomOut" }, { type: "separator" }, { label: t("menu.fullscreen"), role: "togglefullscreen" }, ], }); } // menuTemplate.push({ // label: t("帮助"), // role: "help", // submenu: [ // // { // // label: t("教程帮助"), // // click: () => { // // createHelpWindow(); // // }, // // }, // // {type: "separator"}, // // { // // label: t("关于"), // // click: () => { // // PageAbout.open().then() // // }, // // }, // ], // }) const menu = Menu.buildFromTemplate(menuTemplate); Menu.setApplicationMenu(menu); }; export const ConfigMenu = { ready, }; ================================================ FILE: electron/config/tray.ts ================================================ import { app, Menu, shell, Tray } from "electron"; import { trayPath } from "./icon"; import { AppRuntime } from "../mapi/env"; import { AppConfig } from "../../src/config"; import { t } from "./lang"; import { isMac, isWin } from "../lib/env"; import { AppsMain } from "../mapi/app/main"; let tray = null; const showApp = () => { AppRuntime.mainWindow.show(); AppRuntime.mainWindow.focus(); }; const hideApp = () => { if (isMac) { app.dock.hide(); } AppRuntime.mainWindow.hide(); }; const quitApp = () => { (app as any).forceQuit = true; app.quit(); }; const ready = () => { const contextMenu = Menu.buildFromTemplate([ { label: t("tray.showMain"), click: () => { showApp(); }, }, { label: t("nav.guide"), click: () => { AppsMain.windowOpen("guide").then(); }, }, { label: t("tray.visitWebsite"), click: () => { shell.openExternal(AppConfig.website); }, }, { type: "separator" }, { label: t("tray.restart"), click: () => { app.relaunch(); quitApp(); }, }, { label: t("menu.quit"), click: () => { quitApp(); }, }, { type: "separator" }, { label: t("about.title"), click: () => { AppsMain.windowOpen("about").then(); }, }, ]); tray = new Tray(trayPath); tray.setToolTip(AppConfig.name); tray.on("click", () => { showApp(); }); tray.on("right-click", () => { tray.popUpContextMenu(contextMenu); }); }; const show = () => { if (tray) { tray.destroy(); tray = null; } }; export const ConfigTray = { ready, }; ================================================ FILE: electron/config/window.ts ================================================ export const WindowConfig = { alwaysOpenDevTools: true, minWidth: 800, minHeight: 60, initWidth: 800, initHeight: 60, mainHeight: 60, mainWidth: 800, mainMaxHeight: 600, pluginWidth: 800, pluginHeight: 500, aboutWidth: 500, aboutHeight: 400, logWidth: 800, logHeight: 600, feedbackWidth: 700, feedbackHeight: 600, guideWidth: 800, guideHeight: 540, paymentWidth: 500, paymentHeight: 400, setupWidth: 800, setupHeight: 540, fastPanelWidth: 260, fastPanelHeight: 500, detachWindowTitleHeight: 40, }; ================================================ FILE: electron/declarations/electron.d.ts ================================================ declare module "electron" { interface BrowserView { _window?: any; _plugin?: any; } interface BrowserWindow { _name?: string; _plugin?: any; _type?: "action" | "callPage"; } } ================================================ FILE: electron/declarations/svg.d.ts ================================================ declare module "*.svg" { const content: string; export default content; } ================================================ FILE: electron/electron-env.d.ts ================================================ /// /// declare namespace NodeJS { interface ProcessEnv { /** * The built directory structure * * ```tree * ├─┬ dist-electron * │ ├─┬ main * │ │ └── index.js > Electron-Main * │ └─┬ preload * │ └── index.mjs > Preload-Scripts * ├─┬ dist * │ └── index.html > Electron-Renderer * ``` */ APP_ROOT: string; /** /dist/ or /public/ */ VITE_PUBLIC: string; } } ================================================ FILE: electron/lib/api.ts ================================================ import Apps from "../mapi/app"; export type ResultType = { // should follow the rules: // <0 business error // =0 success // 10000 error ( network error, server error, etc. ) code: number; msg: string; data?: T; }; export const post = async (url: string, data: any) => { data = data || {}; const userAgent = Apps.getUserAgent(); data["AppManagerUserAgent"] = userAgent; return await fetch(url, { method: "POST", headers: { "User-Agent": userAgent, "Content-Type": "application/json", }, body: JSON.stringify(data), }); }; ================================================ FILE: electron/lib/devtools.ts ================================================ import { BrowserView, BrowserWindow, screen } from "electron"; import { isDev } from "./env"; import { WindowConfig } from "../config/window"; export const DevToolsManager = { enable: true, rowCount: 4, colCount: 3, windows: new Map(), setEnable(enable: boolean) { DevToolsManager.enable = enable; }, getWindow(win: BrowserWindow | BrowserView) { return this.windows.get(win); }, getOrCreateWindow(name: string, win: BrowserWindow | BrowserView) { if (this.windows.has(win)) { return this.windows.get(win); } const { x, y, width, height } = this.getDisplayPosition(); // console.log('DevToolsManager', name, {x, y, width, height}) const devtools = new BrowserWindow({ show: true, x, y, width, height, title: name, }); devtools.on("closed", (e) => { // console.log('DevToolsManager', 'close', name) this.windows.delete(win); }); // console.log('DevToolsManager', name, {x, y}) win.webContents.setDevToolsWebContents(devtools.webContents); win.webContents.on("destroyed", () => { // console.log('DevToolsManager', 'destroyed', name) devtools.destroy(); }); devtools.webContents.on("dom-ready", () => { setTimeout(() => { if (!devtools.isDestroyed()) { devtools.setTitle(name); } }, 1000); }); this.windows.set(win, devtools); return devtools; }, getLargestDisplay(): Electron.Display { const displays = screen.getAllDisplays(); return displays.reduce((max, display) => { const { width, height } = display.size; const maxResolution = max.size.width * max.size.height; const currentResolution = width * height; return currentResolution > maxResolution ? display : max; }); }, getDisplayPosition(): { x: number; y: number; width: number; height: number; } { const display = this.getLargestDisplay(); const { x, y, width, height } = display.workArea; // console.log('DevToolsManager', 'getDisplayPosition', {x, y, width, height}) if (width < 1300) { this.rowCount = 3; this.colCount = 2; } const itemWidth = Math.floor(width / this.rowCount); const itemHeight = Math.floor(height / this.colCount); const maxRow = Math.floor(width / itemWidth); const row = this.windows.size % maxRow; const col = Math.floor(this.windows.size / maxRow); return { x: x + row * itemWidth, y: y + col * itemHeight, width: itemWidth, height: itemHeight, }; }, register(name: string, win: BrowserWindow | BrowserView) { if (!isDev || !DevToolsManager.enable) { return; } this.getOrCreateWindow(name, win); }, autoShow(win: BrowserWindow | BrowserView) { if (!isDev || !DevToolsManager.enable) { return; } if (WindowConfig.alwaysOpenDevTools) { win.webContents.openDevTools({ mode: "detach", activate: false, }); } }, }; ================================================ FILE: electron/lib/env-main.ts ================================================ import url, { fileURLToPath } from "node:url"; import { BrowserView, BrowserWindow } from "electron"; import { isPackaged } from "./env"; import path, { join } from "node:path"; import { Log } from "../mapi/log/main"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); process.env.APP_ROOT = path.join(__dirname, "../.."); export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; export const preloadDefault = path.join(MAIN_DIST, "preload/index.cjs"); export const preloadPluginDefault = path.join( MAIN_DIST, "preload-plugin/plugin.cjs", ); export const rendererLoadPath = ( window: BrowserWindow | BrowserView, fileName: string, ) => { if (!isPackaged && process.env.VITE_DEV_SERVER_URL) { const x = new url.URL(rendererDistPath(fileName)); // console.log('rendererLoadPath', fileName, x.toString()) if (window instanceof BrowserView) { window.webContents.loadURL(x.toString()); } else { window.loadURL(x.toString()); } } else { // console.log('rendererLoadPath', fileName, rendererDistPath(fileName)) if (window instanceof BrowserView) { window.webContents.loadFile(rendererDistPath(fileName)); } else { window.loadFile(rendererDistPath(fileName)); } } }; export const rendererDistPath = (fileName: string) => { if (!isPackaged && process.env.VITE_DEV_SERVER_URL) { return `${process.env.VITE_DEV_SERVER_URL.replace(/\/+$/, "")}/${fileName}`; } return join(RENDERER_DIST, fileName); }; export const rendererIsUrl = (url: string) => { return ( url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://") ); }; export const getGpuInfo = async () => { const list = [] as { index: number; name: string; size: number; }[]; try { // @ts-ignore const si = await import("systeminformation"); const graphics = await si.graphics(); graphics.controllers.forEach((controller, index) => { list.push({ index, name: controller.model, size: Math.ceil(controller.vram / 1024), }); }); } catch (e) { Log.error("getGpuInfo", e); } return list; }; ================================================ FILE: electron/lib/env.ts ================================================ import { execSync } from "child_process"; import { resolve } from "node:path"; import fs from "node:fs"; import os from "os"; import { Log } from "../mapi/log"; import FileIndex from "../mapi/file"; export const isPackaged = ["true"].includes(process.env.IS_PACKAGED); export const isDev = !isPackaged; export const isWin = process.platform === "win32"; export const isMac = process.platform === "darwin"; export const isLinux = process.platform === "linux"; export const isMain = process.type === "browser"; export const isRender = process.type === "renderer"; export const platformName = (): "win" | "osx" | "linux" | null => { if (isWin) return "win"; if (isMac) return "osx"; if (isLinux) return "linux"; return null; }; export const memoryInfo = () => { return { total: os.totalmem(), free: os.freemem(), }; }; const tryFirst = (functionList: (() => any)[]) => { for (const fun of functionList) { try { return fun(); } catch (e) {} } return null; }; let platformVersionCache: string | null = null; export const platformVersion = () => { if (null === platformVersionCache) { const functionList: any[] = []; if (isWin) { functionList.push(() => execSync("wmic os get Version") .toString() .split("\n")[1] .trim(), ); functionList.push(() => execSync( "powershell -command \"(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId\"", ) .toString() .trim(), ); } else if (isMac) { functionList.push(() => execSync("sw_vers -productVersion").toString().trim(), ); } else if (isLinux) { functionList.push(() => execSync("cat /etc/os-release | grep VERSION_ID") .toString() .split("=")[1] .trim() .replace(/"/g, ""), ); } platformVersionCache = tryFirst(functionList); if (!platformVersionCache) { Log.error("env.platformVersion.error"); platformVersionCache = "0.0.0"; } } return platformVersionCache; }; export const platformArch = (): "x86" | "arm64" | null => { switch (os.arch()) { case "x64": return "x86"; case "arm64": return "arm64"; } return null; }; let platformUUIDCache: string | null = null; export const platformUUID = () => { if (null === platformUUIDCache) { const functionList: any[] = []; if (isWin) { functionList.push(() => execSync("wmic csproduct get UUID") .toString() .split("\n")[1] .trim(), ); functionList.push(() => execSync( 'powershell -command "(Get-WmiObject Win32_ComputerSystemProduct).UUID"', ) .toString() .trim(), ); } else if (isMac) { functionList.push(() => execSync("system_profiler SPHardwareDataType | grep UUID") .toString() .split(": ")[1] .trim(), ); } else if (isLinux) { functionList.push(() => execSync("cat /var/lib/dbus/machine-id") .toString() .trim() .toUpperCase(), ); } platformUUIDCache = tryFirst(functionList); if (!platformUUIDCache) { Log.error("env.platformUUID.error"); platformUUIDCache = "000000"; } } return platformUUIDCache; }; export const buildResolve = (value: string): string => { return resolve(`electron/resources/build/${value}`); }; export const binResolve = (value: string): string => { return resolve(process.resourcesPath, "bin", value); }; export const extraResolve = (filePath: string): string => { const basePath = isPackaged ? process.resourcesPath : "electron/resources"; return resolve(basePath, "extra", filePath); }; export const extraResolveWithPlatform = (filePath: string): string => { const dir = [platformName(), platformArch()].join("-"); const p = [dir, filePath].join("/"); return extraResolve(p); }; export const extraResolveBin = (filePath: string): string => { if (isWin) { if (!filePath.endsWith(".exe")) { filePath += ".exe"; } } const dir = [platformName(), platformArch()].join("-"); const p = [dir, filePath].join("/"); const binaryPath = extraResolve(p); if (!fs.existsSync(binaryPath)) { throw new Error(`Binary file not found: ${binaryPath}`); } return binaryPath; }; ================================================ FILE: electron/lib/hooks.ts ================================================ import { BrowserWindow } from "electron"; type HookType = never | "Show" | "Hide"; export const executeHooks = async ( win: BrowserWindow, hook: HookType, data?: any, ) => { const evalJs = ` if(window.__page && window.__page.hooks && typeof window.__page.hooks.on${hook} === 'function' ) { try { window.__page.hooks.on${hook}(${JSON.stringify(data)}); } catch(e) { console.log('executeHooks.on${hook}.error', e); } }`; return win.webContents?.executeJavaScript(evalJs); }; ================================================ FILE: electron/lib/permission.ts ================================================ import { isMac } from "./env"; let nodeMacPermissions = null; if (isMac) { (async () => { try { nodeMacPermissions = await import("node-mac-permissions"); nodeMacPermissions = nodeMacPermissions.default; // console.log('nodeMacPermissions',nodeMacPermissions); } catch (e) {} })(); } export const Permissions = { async checkAccessibilityAccess(): Promise { return new Promise((resolve, reject) => { if (isMac) { const status = nodeMacPermissions.getAuthStatus("accessibility"); resolve(status === "authorized"); } else { resolve(true); } }); }, async askAccessibilityAccess() { nodeMacPermissions.askForAccessibilityAccess(); }, async checkScreenCaptureAccess(): Promise { return new Promise((resolve, reject) => { if (isMac) { const status = nodeMacPermissions.getAuthStatus("screen"); resolve(status === "authorized"); } else { resolve(true); } }); }, async askScreenCaptureAccess() { nodeMacPermissions.askForScreenCaptureAccess(true); }, }; ================================================ FILE: electron/lib/pinyin-util.ts ================================================ import PinyinMatch from "pinyin-match"; export const PinyinUtil = { match(input, keywords) { const index = PinyinMatch.match(input, keywords); let inputMark = input; let similarity = 0; if (index) { const indexStart = index[0]; const indexEnd = index[1]; inputMark = input.substring(0, indexStart) + "" + input.substring(indexStart, indexEnd + 1) + "" + input.substring(indexEnd + 1); similarity = (indexEnd - indexStart + 1) / input.length; } return { matched: !!index, inputMark, similarity, }; }, mark(text) { return `${text}`; }, }; ================================================ FILE: electron/lib/process.ts ================================================ /** 在主进程中获取关键信息存储到环境变量中,从而在预加载脚本中及渲染进程中使用 */ import { app } from "electron"; /** 注意: app.isPackaged 可能被被某些方法改变所以请将该文件放到 main.js 必须位于非依赖项的顶部 */ import fixPath from "fix-path"; if (process.platform === "darwin") { fixPath(); } process.env.IS_PACKAGED = String(app.isPackaged); process.env.DESKTOP_PATH = app.getPath("desktop"); process.env.CWD = process.cwd(); export const isDummy = false; ================================================ FILE: electron/lib/util.ts ================================================ import chardet from "chardet"; import dayjs from "dayjs"; import iconvLite from "iconv-lite"; import { Base64 } from "js-base64"; import * as crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import Showdown from "showdown"; import { Iconv } from "iconv"; import { isMac, isWin } from "./env"; import FileIndex from "../mapi/file"; export const sleep = (time = 1000) => { return new Promise((resolve) => { setTimeout(() => resolve(true), time); }); }; export const EncodeUtil = { base32Alphabet: "abcdefghijklmnopqrstuvwxyz234567", base32Encode(str: string) { const buffer = Buffer.from(str, "utf8"); let bits = ""; let output = ""; // 将每个字节转为8位二进制 for (let i = 0; i < buffer.length; i++) { const byte = buffer[i]; bits += byte.toString(2).padStart(8, "0"); } // 每5位一组,转为 Base32 字符 for (let i = 0; i < bits.length; i += 5) { const chunk = bits.slice(i, i + 5); const paddedChunk = chunk.padEnd(5, "0"); // 不足5位补0 const index = parseInt(paddedChunk, 2); output += EncodeUtil.base32Alphabet[index]; } return output; }, base32Decode(str: string) { const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"; let bits = ""; for (let i = 0; i < str.length; i++) { const char = str[i]; const index = base32Alphabet.indexOf(char); if (index === -1) { throw new Error("Invalid Base32 character: " + char); } bits += index.toString(2).padStart(5, "0"); } const bytes: number[] = []; for (let i = 0; i + 8 <= bits.length; i += 8) { const byte = bits.slice(i, i + 8); bytes.push(parseInt(byte, 2)); } return Buffer.from(bytes).toString("utf8"); }, base64Encode(str: string) { return Base64.encode(str); }, base64Decode(str: string) { return Base64.decode(str); }, md5(str: string) { return crypto.createHash("md5").update(str).digest("hex"); }, aesEncode(str: string, key: string) { const cipher = crypto.createCipheriv("aes-128-ecb", key, ""); let crypted = cipher.update(str, "utf8", "base64"); crypted += cipher.final("base64"); return crypted; }, aesDecode(str: string, key: string) { const decipher = crypto.createDecipheriv("aes-128-ecb", key, ""); let dec = decipher.update(str, "base64", "utf8"); dec += decipher.final("utf8"); return dec; }, async fileXzipEncode(pathname: string): Promise { if (!fs.existsSync(pathname)) { throw new Error(`Input file not found: ${pathname}`); } // Generate new filepath with .xzip extension const basePath = pathname.substring(0, pathname.lastIndexOf(".")); const outputPath = basePath + ".xzip"; // Get file info const fileStats = fs.statSync(pathname); const fileSize = fileStats.size; const fileExt = pathname.split(".").pop() || ""; // Generate random 16-character key const encryptionKey = StrUtil.randomString(16); // Create metadata const filemeta = { version: 1, format: fileExt, size: fileSize, key: encryptionKey, }; // Convert metadata to JSON and then base64 encode const metaJson = JSON.stringify(filemeta); const metaB64 = Buffer.from(metaJson, "utf-8").toString("base64"); const metaLength = metaB64.length; // Prepare encryption key const keyBytes = Buffer.from(encryptionKey, "utf-8"); const keyLength = keyBytes.length; // Stream processing: read, encrypt and write in chunks const inputStream = fs.createReadStream(pathname); const outputStream = fs.createWriteStream(outputPath); // Write metadata length (4 bytes, little-endian) const metaLengthBuffer = Buffer.allocUnsafe(4); metaLengthBuffer.writeUInt32LE(metaLength, 0); outputStream.write(metaLengthBuffer); // Write base64 encoded metadata outputStream.write(Buffer.from(metaB64, "utf-8")); // Stream encrypt the file content let bytesProcessed = 0; return new Promise((resolve, reject) => { inputStream.on("data", (chunk: Buffer) => { // XOR encrypt the chunk const encryptedChunk = Buffer.alloc(chunk.length); for (let i = 0; i < chunk.length; i++) { encryptedChunk[i] = chunk[i] ^ keyBytes[bytesProcessed % keyLength]; bytesProcessed++; } // Write encrypted chunk outputStream.write(encryptedChunk); }); inputStream.on("end", () => { outputStream.end(); resolve(outputPath); }); inputStream.on("error", (error) => { outputStream.destroy(); reject(error); }); outputStream.on("error", (error) => { inputStream.destroy(); reject(error); }); }); }, async fileXzipDecode(pathname: string): Promise { if (!fs.existsSync(pathname)) { throw new Error(`Input file not found: ${pathname}`); } if (!pathname.endsWith(".xzip")) { return pathname; // Not an xzip file, return as is } let outputPath = pathname.replace(/\.xzip$/, ""); return new Promise((resolve, reject) => { const inputStream = fs.createReadStream(pathname); let metadataRead = false; let filemeta: any = null; let keyBytes: Buffer; let bytesProcessed = 0; let outputStream: fs.WriteStream; let remainingMetaBytes = 0; let metaBuffer = Buffer.alloc(0); inputStream.on("data", (chunk: Buffer) => { let chunkOffset = 0; if (!metadataRead) { if (remainingMetaBytes === 0) { // Read metadata length (first 4 bytes) if (chunk.length < 4) { reject( new Error( "Invalid xzip file: insufficient data for metadata length", ), ); return; } const metaLength = chunk.readUInt32LE(0); remainingMetaBytes = metaLength; chunkOffset = 4; } // Read metadata const availableMetaBytes = Math.min( remainingMetaBytes, chunk.length - chunkOffset, ); const metaChunk = chunk.subarray( chunkOffset, chunkOffset + availableMetaBytes, ); metaBuffer = Buffer.concat([ metaBuffer, metaChunk, ] as readonly Uint8Array[]); remainingMetaBytes -= availableMetaBytes; chunkOffset += availableMetaBytes; if (remainingMetaBytes === 0) { // Parse metadata try { const metaB64 = metaBuffer.toString("utf-8"); const metaJson = Buffer.from( metaB64, "base64", ).toString("utf-8"); filemeta = JSON.parse(metaJson); keyBytes = Buffer.from(filemeta.key, "utf-8"); // Create output file with correct extension const finalOutputPath = outputPath + (filemeta.format ? "." + filemeta.format : ""); outputStream = fs.createWriteStream(finalOutputPath); metadataRead = true; // Set the final output path for resolution outputPath = finalOutputPath; } catch (error) { reject( new Error( "Invalid xzip file: corrupted metadata", ), ); return; } } } if (metadataRead && chunkOffset < chunk.length) { // Decrypt remaining chunk data const encryptedChunk = chunk.subarray(chunkOffset); const decryptedChunk = Buffer.alloc(encryptedChunk.length); const keyLength = keyBytes.length; for (let i = 0; i < encryptedChunk.length; i++) { decryptedChunk[i] = encryptedChunk[i] ^ keyBytes[bytesProcessed % keyLength]; bytesProcessed++; } outputStream.write(decryptedChunk); } }); inputStream.on("end", () => { if (outputStream) { outputStream.end(); resolve(outputPath); } else { reject(new Error("Invalid xzip file: incomplete metadata")); } }); inputStream.on("error", (error) => { if (outputStream) { outputStream.destroy(); } reject(error); }); if (outputStream) { outputStream.on("error", (error) => { inputStream.destroy(); reject(error); }); } }); }, }; export const IconvUtil = { convert(str: string, to?: string, from?: string) { if (!from) { from = chardet.detect(Buffer.from(str)); } to = to || "utf8"; const buffer = iconvLite.encode(str, from); return iconvLite.decode(buffer, to); }, bufferToUtf8(buffer: Buffer) { const encoding = chardet.detect(buffer); // console.log('bufferToUtf8.encoding', encoding) if ("ISO-2022-CN" === encoding) { const iconvInstance = new Iconv( "ISO-2022-CN", "UTF-8//TRANSLIT//IGNORE", ); return iconvInstance.convert(buffer).toString(); } return iconvLite.decode(buffer, encoding).toString(); }, detect(buffer: Uint8Array) { // detect str encoding return chardet.detect(buffer); }, }; export const StrUtil = { randomString(len: number = 32) { const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; for (let i = len; i > 0; --i) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; }, uuid() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( /[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }, ); }, hashCode(str: string, length: number = 8) { let hash = 0; if (str.length === 0) return hash + ""; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } let result = Math.abs(hash).toString(16); if (result.length < length) { result = "0".repeat(length - result.length) + result; } return result; }, hashCodeWithDuplicateCheck( str: string, check: string[], length: number = 8, ) { let code = this.hashCode(str, length); while (check.includes(code)) { code = this.uuid().substring(0, length); } return code; }, bigIntegerId() { return [ Date.now(), (Math.floor(Math.random() * 1000000) + "").padStart(6, "0"), ].join(""); }, ucFirst(str: string) { if (!str) return ""; return str.charAt(0).toUpperCase() + str.slice(1); }, }; export const TimeUtil = { timestampInMs() { return Date.now(); }, timestamp() { return Math.floor(Date.now() / 1000); }, format(time: number, format: string = "YYYY-MM-DD HH:mm:ss") { return dayjs(time).format(format); }, formatDate(time: number) { return dayjs(time).format("YYYY-MM-DD"); }, dateString() { return dayjs().format("YYYYMMDD"); }, datetimeString() { return dayjs().format("YYYYMMDD_HHmmss_SSS"); }, timestampDayStart(msTimestamp?: number) { let date = msTimestamp ? new Date(msTimestamp) : new Date(); date.setHours(0, 0, 0, 0); return Math.floor(date.getTime() / 1000); }, replacePattern(text: string) { // @ts-ignore return text .replaceAll("{year}", dayjs().format("YYYY")) .replaceAll("{month}", dayjs().format("MM")) .replaceAll("{day}", dayjs().format("DD")) .replaceAll("{hour}", dayjs().format("HH")) .replaceAll("{minute}", dayjs().format("mm")) .replaceAll("{second}", dayjs().format("ss")); }, }; export const FileUtil = { MIME_TYPES: { html: "text/html", htm: "text/html", js: "application/javascript", css: "text/css", json: "application/json", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", mp3: "audio/mpeg", mp4: "video/mp4", wav: "audio/wav", wasm: "application/wasm", eot: "application/vnd.ms-fontobject", }, getMimeByExt(ext: string, defaultMime: string = ""): string { ext = ext.toLowerCase(); if (ext.startsWith(".")) { ext = ext.substring(1); } return FileUtil.MIME_TYPES[ext] || defaultMime; }, getMimeByPath(p: string, defaultMime: string = ""): string { const extension = p.split(".").pop().toLowerCase(); return FileUtil.getMimeByExt(extension, defaultMime); }, streamToBase64(stream: NodeJS.ReadableStream): Promise { return new Promise((resolve, reject) => { const chunks = []; stream.on("data", (chunk) => { chunks.push(chunk); }); stream.on("end", () => { const buffer = Buffer.concat(chunks); resolve(buffer.toString("base64")); }); stream.on("error", (error) => { reject(error); }); }); }, bufferToBase64(buffer: Buffer) { let binary = ""; let bytes = new Uint8Array(buffer); let len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return EncodeUtil.base64Encode(binary); }, base64ToBuffer(base64: string): Buffer { if (base64.startsWith("data:")) { base64 = base64.split("base64,")[1]; } return Buffer.from(base64, "base64"); }, formatSize(size: number) { if (size < 1024) { return size + "B"; } else if (size < 1024 * 1024) { return (size / 1024).toFixed(2) + "KB"; } else if (size < 1024 * 1024 * 1024) { return (size / 1024 / 1024).toFixed(2) + "MB"; } else { return (size / 1024 / 1024 / 1024).toFixed(2) + "GB"; } }, async md5(filePath: string) { return new Promise((resolve, reject) => { const hash = crypto.createHash("md5"); const stream = fs.createReadStream(filePath); stream.on("data", (data) => { hash.update(data); }); stream.on("end", () => { resolve(hash.digest("hex")); }); stream.on("error", (error) => { reject(error); }); }); }, }; export const JsonUtil = { stringifyOrdered(obj: any) { return JSON.stringify(obj, Object.keys(obj).sort(), 4); }, stringifyValueOrdered(obj: any) { const sortedData = Object.fromEntries( Object.entries(obj).sort(([, a], [, b]) => { // @ts-ignore return ((a as any) - b) as any; }), ); return JSON.stringify(sortedData, null, 4); }, }; export const ImportUtil = { async loadCommonJs(cjsPath: string, forceReload: boolean = true) { let tempPath = cjsPath; if (forceReload) { const md5 = await FileUtil.md5(cjsPath); tempPath = path.join( await FileIndex.tempDir("commonJs"), `${md5}.cjs`, ); if (!fs.existsSync(tempPath)) { fs.copyFileSync(cjsPath, tempPath); } } const backend = await import(/* @vite-ignore */ `file://${tempPath}`); // console.log('loadCommonJs', `file://${cjsPath}?t=${md5}`) return backend.default; }, }; export const MemoryCacheUtil = { pool: {} as { [key: string]: { value: any; expire: number; }; }, _gc() { const now = TimeUtil.timestamp(); for (const key in this.pool) { if (this.pool[key].expire < now) { delete this.pool[key]; } } }, async remember( key: string, callback: () => Promise, ttl: number = 3600, ) { if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) { return this.pool[key].value as T; } const value = await callback(); this.pool[key] = { value, expire: TimeUtil.timestamp() + ttl, }; this._gc(); return value as T; }, get(key: string) { if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) { return this.pool[key].value; } this._gc(); return null; }, set(key: string, value: any, ttl: number = 86400) { this.pool[key] = { value, expire: TimeUtil.timestamp() + ttl, }; this._gc(); }, forget(key: string) { delete this.pool[key]; }, }; export const MemoryMapCacheUtil = { pool: {} as { [group: string]: { [key: string]: { value: any; expire: number; }; }; }, _gc() { const now = TimeUtil.timestamp(); for (const group in this.pool) { for (const key in this.pool[group]) { if (this.pool[group][key].expire < now) { delete this.pool[group][key]; } } } }, get(group: string, key: string) { if ( this.pool[group] && this.pool[group][key] && this.pool[group][key].expire > TimeUtil.timestamp() ) { return this.pool[group][key].value; } this._gc(); return null; }, set(group: string, key: string, value: any, ttl: number = 86400) { if (!this.pool[group]) { this.pool[group] = {}; } this.pool[group][key] = { value, expire: TimeUtil.timestamp() + ttl, }; this._gc(); }, forget(group: string, key: string) { if (this.pool[group]) { delete this.pool[group][key]; } }, }; export const ShellUtil = { quotaPath(p: string) { return `"${p}"`; }, parseCommandArgs(command: string) { let args = []; let arg = ""; let quote = ""; let escape = false; for (let i = 0; i < command.length; i++) { const c = command[i]; if (escape) { arg += c; escape = false; continue; } if ("\\" === c) { escape = true; arg += c; continue; } if ("" === quote && (" " === c || "\t" === c)) { if (arg) { args.push(arg); arg = ""; } continue; } if ("" === quote && ('"' === c || "'" === c)) { quote = c; arg += c; continue; } if ('"' === quote && '"' === c) { quote = ""; arg += c; continue; } if ("'" === quote && "'" === c) { quote = ""; arg += c; continue; } arg += c; } if (arg) { args.push(arg); } return args; }, }; export const VersionUtil = { /** * 检测版本是否匹配 * @param v string * @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0 */ match(v: string, match: string) { if (match === "*") { return true; } if (match.startsWith(">=")) { if (this.ge(v, match.substring(2))) { return true; } } else if (match.startsWith("<=")) { if (this.le(v, match.substring(2))) { return true; } } else if (match.startsWith(">")) { if (this.gt(v, match.substring(1))) { return true; } } else if (match.startsWith("<")) { if (this.lt(v, match.substring(1))) { return true; } } else { return this.eq(v, match); } return false; }, compare(v1: string, v2: string) { const v1Arr = v1.split("."); const v2Arr = v2.split("."); for (let i = 0; i < v1Arr.length; i++) { const v1Num = parseInt(v1Arr[i]); const v2Num = parseInt(v2Arr[i]); if (v1Num > v2Num) { return 1; } else if (v1Num < v2Num) { return -1; } } return 0; }, gt(v1: string, v2: string) { return VersionUtil.compare(v1, v2) > 0; }, ge(v1: string, v2: string) { return VersionUtil.compare(v1, v2) >= 0; }, lt(v1: string, v2: string) { return VersionUtil.compare(v1, v2) < 0; }, le: (v1: string, v2: string) => { return VersionUtil.compare(v1, v2) <= 0; }, eq: (v1: string, v2: string) => { return VersionUtil.compare(v1, v2) === 0; }, }; export const UIUtil = { sizeToPx(size: string, sizeFull: number) { if (/^\d+$/.test(size)) { // 纯数字 return parseInt(size); } else if (size.endsWith("%")) { // 百分比 let result = Math.floor((sizeFull * parseInt(size)) / 100); result = Math.min(result, sizeFull); return result; } else { throw "UnsupportSizeString"; } }, }; export const ReUtil = { match(regex: string, text: string) { if ("" === regex || null === regex) { return false; } if (regex.startsWith("/")) { const index = regex.lastIndexOf("/"); const source = regex.slice(1, index); const flags = regex.slice(index + 1); return new RegExp(source, flags).test(text); } return new RegExp(regex).test(text); }, }; const converter = new Showdown.Converter({ tables: true, }); export const MarkdownUtil = { toHtml(markdown: string): string { return converter.makeHtml(markdown); }, }; type HotkeyModifierType = | "Control" | "Option" | "Command" | "Ctrl" | "Alt" | "Win" | "Meta" | "Shift"; type HotkeyType = { key: string; modifiers: HotkeyModifierType[] }; export const HotKeyUtil = { orderModifiers(modifiers: HotkeyModifierType[]) { const order = [ "Control", "Ctrl", "Command", "Meta", "Win", "Option", "Alt", "Shift", ]; return modifiers.sort((a, b) => { return order.indexOf(a) - order.indexOf(b); }); }, unifyObject(hotkey: HotkeyType) { return { key: hotkey.key.toUpperCase(), modifiers: this.orderModifiers( hotkey.modifiers.map((modifier) => StrUtil.ucFirst(modifier)), ), }; }, unifyString(hotkey: string): HotkeyType { const parts = hotkey.split("+"); const key = parts.pop() || ""; const modifiers: any[] = []; parts.forEach((part) => { modifiers.push(StrUtil.ucFirst(part.trim())); }); return this.unifyObject({ key, modifiers }); }, unify( hotkeys: string | string[] | HotkeyType | HotkeyType[], ): HotkeyType[] { if (typeof hotkeys === "string") { return [this.unifyString(hotkeys)]; } else if (Array.isArray(hotkeys)) { return hotkeys.map((hotkey) => { if (typeof hotkey === "string") { return this.unifyString(hotkey); } else { return this.unifyObject(hotkey); } }); } else { return [this.unifyObject(hotkeys)]; } }, getFromEvent(event: any): HotkeyType | null { const valid = [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Space", ]; const key = (event.key || "").toUpperCase(); if (!event || !event.key || !valid.includes(key)) { return null; } const modifiers: HotkeyModifierType[] = []; if (isWin) { if (event.ctrlKey || event.control) { modifiers.push("Ctrl"); } if (event.altKey || event.alt) { modifiers.push("Alt"); } if (event.metaKey || event.meta) { modifiers.push("Win"); } } else if (isMac) { if (event.ctrlKey || event.control) { modifiers.push("Control"); } if (event.altKey || event.alt) { modifiers.push("Option"); } if (event.metaKey || event.meta) { modifiers.push("Command"); } } else { if (event.ctrlKey || event.control) { modifiers.push("Ctrl"); } if (event.altKey || event.alt) { modifiers.push("Alt"); } if (event.metaKey || event.meta) { modifiers.push("Meta"); } } if (event.shiftKey || event.shift) { modifiers.push("Shift"); } return this.unifyObject({ key, modifiers }); }, match(hotkeysForMatch: HotkeyType[], hotkey: HotkeyType): boolean { if (!hotkeysForMatch || !hotkey) { return false; } const hotKeyStr = hotkey.modifiers.join("+") + "+" + hotkey.key; for (const key of hotkeysForMatch) { const keyStr = key.modifiers.join("+") + "+" + key.key; if (keyStr === hotKeyStr) { return true; } } return false; }, }; ================================================ FILE: electron/main/fastPanel.ts ================================================ import { icnsLogoPath, icoLogoPath, logoPath } from "../config/icon"; import { AppRuntime } from "../mapi/env"; import { AppConfig } from "../../src/config"; import { isPackaged } from "../lib/env"; import { WindowConfig } from "../config/window"; import { preloadDefault, RENDERER_DIST, rendererLoadPath, VITE_DEV_SERVER_URL, } from "../lib/env-main"; import * as remoteMain from "@electron/remote/main"; import { Page } from "../page"; import { BrowserWindow } from "electron"; import path from "node:path"; import { executeHooks } from "../mapi/manager/lib/hooks"; import { DevToolsManager } from "../lib/devtools"; import ConfigMain from "../mapi/config/main"; export const FastPanelMain = { init() { const fastPanelHtml = path.join(RENDERER_DIST, "page/fastPanel.html"); let icon = logoPath; if (process.platform === "win32") { icon = icoLogoPath; } else if (process.platform === "darwin") { icon = icnsLogoPath; } AppRuntime.fastPanelWindow = new BrowserWindow({ show: false, title: AppConfig.name, ...(!isPackaged ? { icon } : {}), frame: false, transparent: false, hasShadow: true, center: true, useContentSize: true, minWidth: WindowConfig.fastPanelWidth, minHeight: WindowConfig.fastPanelHeight, width: WindowConfig.fastPanelWidth, height: WindowConfig.fastPanelHeight, skipTaskbar: true, resizable: false, maximizable: false, backgroundColor: "#f1f5f9", alwaysOnTop: true, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, contextIsolation: false, sandbox: false, webSecurity: false, webviewTag: true, }, }); AppRuntime.fastPanelWindow.on("closed", () => { AppRuntime.fastPanelWindow = null; }); AppRuntime.fastPanelWindow.on("show", async () => { await executeHooks(AppRuntime.fastPanelWindow, "Show"); }); AppRuntime.fastPanelWindow.on("hide", async () => { await executeHooks(AppRuntime.fastPanelWindow, "Hide"); }); AppRuntime.fastPanelWindow.on("blur", async () => { const fastPanelAutoHide = await ConfigMain.getEnv( "fastPanelAutoHide", true, ); if (fastPanelAutoHide) { AppRuntime.fastPanelWindow.hide(); } }); rendererLoadPath(AppRuntime.fastPanelWindow, "page/fastPanel.html"); remoteMain.enable(AppRuntime.fastPanelWindow.webContents); AppRuntime.fastPanelWindow.webContents.on("did-finish-load", () => { Page.ready("fastPanel"); DevToolsManager.autoShow(AppRuntime.fastPanelWindow); }); DevToolsManager.register("FastPanel", AppRuntime.fastPanelWindow); // AppRuntime.fastPanelWindow.webContents.setWindowOpenHandler(({url}) => { // if (url.startsWith('https:')) shell.openExternal(url) // return {action: 'deny'} // }) }, }; ================================================ FILE: electron/main/index.ts ================================================ import { app, BrowserWindow, desktopCapturer, session, shell } from "electron"; import { optimizer } from "@electron-toolkit/utils"; import path from "node:path"; import fs from "node:fs"; /** process.js 必须位于非依赖项的顶部 */ import { isDummy } from "../lib/process"; import * as remoteMain from "@electron/remote/main"; import { AppEnv, AppRuntime } from "../mapi/env"; import { MAPI } from "../mapi/main"; import { WindowConfig } from "../config/window"; import { AppConfig } from "../../src/config"; import Log from "../mapi/log/main"; import { ConfigMenu } from "../config/menu"; import { ConfigLang } from "../config/lang"; import { ConfigContextMenu } from "../config/contextMenu"; import { preloadDefault, rendererLoadPath } from "../lib/env-main"; import { Page } from "../page"; import { ConfigTray } from "../config/tray"; import { icnsLogoPath, icoLogoPath, logoPath } from "../config/icon"; import { isMac, isPackaged } from "../lib/env"; import { FastPanelMain } from "./fastPanel"; import { executeHooks } from "../mapi/manager/lib/hooks"; import { AppPosition } from "../mapi/app/lib/position"; import { DevToolsManager } from "../lib/devtools"; import { reportError } from "../mapi/log/beacon"; import { AppsMain } from "../mapi/app/main"; import { ManagerEditor } from "../mapi/manager/editor"; import { ProtocolMain } from "../mapi/protocol/main"; app.commandLine.appendSwitch("enable-experimental-web-platform-features"); const isDummyNew = isDummy; if (process.env["ELECTRON_ENV_PROD"]) { DevToolsManager.setEnable(false); } const logDebugContent = (label: string, content: any) => { const filePath = AppEnv.userData + "/debug.log"; const msg = label + " - " + JSON.stringify(content); console.log(msg); fs.appendFileSync(filePath, msg + "\n"); }; process.on("uncaughtException", (reason) => { let error: any = reason; if (error instanceof Error) { error = [error.message, error.stack].join("\n"); } Log.error("UncaughtException", error); reportError( reason instanceof Error ? reason.message : String(reason), reason instanceof Error ? reason.stack : undefined, ); }); process.on("unhandledRejection", (reason) => { let error: any = reason; if (error instanceof Error) { error = [error.message, error.stack].join("\n"); } Log.error("UnhandledRejection", error); reportError( reason instanceof Error ? (reason as Error).message : String(reason), reason instanceof Error ? (reason as Error).stack : undefined, ); }); // Set application name for Windows 10+ notifications if (process.platform === "win32") app.setAppUserModelId(app.getName()); if (!app.requestSingleInstanceLock()) { app.quit(); process.exit(0); } // app.disableHardwareAcceleration(); // app.setAccessibilitySupportEnabled(true) AppEnv.appRoot = process.env.APP_ROOT; AppEnv.appData = app.getPath("appData"); AppEnv.userData = app.getPath("userData"); AppEnv.dataRoot = path.join(AppEnv.userData, "data"); if (!fs.existsSync(AppEnv.dataRoot)) { fs.mkdirSync(AppEnv.dataRoot, { recursive: true }); } for (const dir of ["logs", "storage"]) { if (!fs.existsSync(path.join(AppEnv.dataRoot, dir))) { fs.mkdirSync(path.join(AppEnv.dataRoot, dir), { recursive: true }); } } AppEnv.isInit = true; MAPI.init(); ConfigContextMenu.init(); Log.info("Starting"); Log.info("LaunchInfo", { isPackaged, appRoot: AppEnv.appRoot, appData: AppEnv.appData, userData: AppEnv.userData, dataRoot: AppEnv.dataRoot, }); async function createWindow() { let icon = logoPath; if (process.platform === "win32") { icon = icoLogoPath; } else if (process.platform === "darwin") { icon = icnsLogoPath; } const { x: wx, y: wy } = AppPosition.get( "main", (screenX, screenY, screenWidth, screenHeight) => { // console.log('calculator', {screenX, screenY, screenWidth, screenHeight}); return { x: screenX + screenWidth / 2 - WindowConfig.mainWidth / 2, y: screenY + screenHeight / 8, }; }, ); AppRuntime.mainWindow = new BrowserWindow({ show: true, title: AppConfig.title, ...(!isPackaged ? { icon } : {}), frame: false, transparent: true, hasShadow: true, // center: true, x: wx, y: wy, useContentSize: true, minWidth: WindowConfig.mainWidth, minHeight: WindowConfig.mainHeight, width: WindowConfig.mainWidth, height: WindowConfig.mainHeight, skipTaskbar: true, resizable: false, maximizable: false, backgroundColor: await AppsMain.defaultDarkModeBackgroundColor(), webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, // sandbox: false, }, }); AppRuntime.mainWindow.on("closed", () => { AppRuntime.mainWindow = null; }); AppRuntime.mainWindow.on("show", async () => { await executeHooks(AppRuntime.mainWindow, "Show"); }); AppRuntime.mainWindow.on("hide", async () => { await executeHooks(AppRuntime.mainWindow, "Hide"); }); rendererLoadPath(AppRuntime.mainWindow, "index.html"); remoteMain.enable(AppRuntime.mainWindow.webContents); AppRuntime.mainWindow.webContents.on("did-finish-load", () => { Page.ready("main"); DevToolsManager.autoShow(AppRuntime.mainWindow); }); AppRuntime.mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith("https://") || url.startsWith("http://")) { shell.openExternal(url); } return { action: "deny" }; }); DevToolsManager.register("Main", AppRuntime.mainWindow); FastPanelMain.init(); } const handleArgsForApp = (argv: string[]) => { let filePath = null; let url = null; for (let i = 1; i < argv.length; i++) { const arg = argv[i]; if (arg.startsWith("--")) { continue; } if (["."].includes(arg)) { continue; } if (arg.startsWith("focusany://")) { url = arg; continue; } filePath = arg; break; } if (filePath) { ManagerEditor.openQueue(filePath).then(); } if (url) { ProtocolMain.queue(url).then(); } }; app.on("open-file", (event, path) => { event.preventDefault(); ManagerEditor.openQueue(path).then(); }); app.on("open-url", (event, url) => { event.preventDefault(); ProtocolMain.queue(url).then(); }); app.whenReady() .then(() => { const isRegistered = app.setAsDefaultProtocolClient("focusany"); Log.info("ProtocolRegistered", isRegistered); remoteMain.initialize(); session.defaultSession.setDisplayMediaRequestHandler( (request, callback) => { desktopCapturer .getSources({ types: ["screen"] }) .then((sources) => { // Grant access to the first screen found. callback({ video: sources[0], audio: "loopback" }); }); }, ); }) .then(ConfigLang.readyAsync) .then(() => { if (isMac) { app.dock.hide(); } MAPI.ready(); ConfigMenu.ready(); ConfigTray.ready(); app.on("browser-window-created", (_, window) => { optimizer.watchWindowShortcuts(window); }); createWindow().then(); handleArgsForApp(process.argv); }); app.on("before-quit", (event) => { if (!(app as any).forceQuit && isPackaged) { event.preventDefault(); } }); app.on("will-quit", () => { MAPI.destroy(); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); app.on("second-instance", (event, argv) => { if (AppRuntime.mainWindow) { if (AppRuntime.mainWindow.isMinimized()) { AppRuntime.mainWindow.restore(); } AppRuntime.mainWindow.show(); AppRuntime.mainWindow.focus(); } handleArgsForApp(argv); }); app.on("activate", () => { const allWindows = BrowserWindow.getAllWindows(); if (allWindows.length) { if (!AppRuntime.mainWindow.isVisible()) { AppRuntime.mainWindow.show(); } AppRuntime.mainWindow.focus(); } else { createWindow().then(); } }); ================================================ FILE: electron/mapi/app/icons.ts ================================================ export const icons = { success: '', error: '', info: '', loading: ` `, }; ================================================ FILE: electron/mapi/app/index.ts ================================================ import iconv from "iconv-lite"; import { exec as _exec, spawn } from "node:child_process"; import net from "node:net"; import util from "node:util"; import { AppConfig } from "../../../src/config"; import { extraResolveBin, isLinux, isMac, isWin, platformArch, platformName, platformUUID, platformVersion, } from "../../lib/env"; import { IconvUtil, ShellUtil, StrUtil } from "../../lib/util"; import { Log } from "../log/index"; const exec = util.promisify(_exec); const outputStringConvert = (outputEncoding: "utf8" | "cp936", data: any) => { if (!data) { return ""; } if (outputEncoding === "utf8") { return data.toString(); } let dataEncoding = "binary"; if (Buffer.isBuffer(data)) { dataEncoding = IconvUtil.detect(data as any); if ("UTF-8" === dataEncoding) { return data.toString("utf8"); } } // dataEncoding UTF-8 cp936 // dataEncoding ISO-8859-1 cp936 // console.log('dataEncoding', dataEncoding, outputEncoding) return iconv.decode(Buffer.from(data, dataEncoding as any), outputEncoding); }; const shell = async ( command: string, option?: { cwd?: string; outputEncoding?: string; shell?: boolean; }, ) => { option = Object.assign( { cwd: process.cwd(), outputEncoding: isWin ? "cp936" : "utf8", shell: true, }, option, ); const result = await exec(command, { env: { ...process.env }, shell: option.shell, encoding: "binary", cwd: option["cwd"], } as any); return { stdout: outputStringConvert( option.outputEncoding as any, result.stdout, ), stderr: outputStringConvert( option.outputEncoding as any, result.stderr, ), }; }; const spawnShell = async ( command: string | string[], option: { stdout?: (data: string, process: any) => void; stderr?: (data: string, process: any) => void; success?: (process: any) => void; error?: (msg: string, exitCode: number, process: any) => void; cwd?: string; outputEncoding?: string; env?: Record; shell?: boolean; } | null = null, ): Promise<{ stop: () => void; send: (data: any) => void; result: () => Promise; }> => { option = Object.assign( { cwd: process.cwd(), outputEncoding: isWin ? "cp936" : "utf8", env: {}, shell: true, }, option, ); let commandEntry = "", args = []; if (Array.isArray(command)) { commandEntry = command[0]; args = command.slice(1); } else { args = ShellUtil.parseCommandArgs(command); commandEntry = args.shift() as string; } Log.info("App.spawnShell", { commandEntry, args, option: { cwd: option["cwd"], outputEncoding: option["outputEncoding"], }, }); const spawnProcess = spawn(commandEntry, args, { env: { ...process.env, ...option.env }, cwd: option["cwd"], shell: option.shell, encoding: "binary", } as any); let end = false; let isSuccess = false; let exitCode = -1; const stdoutList: string[] = []; const stderrList: string[] = []; spawnProcess.stdout?.on("data", (data) => { // console.log('App.spawnShell.stdout', data) let dataString = outputStringConvert( option.outputEncoding as any, data, ); Log.info("App.spawnShell.stdout", dataString); stdoutList.push(dataString); option.stdout?.(dataString, spawnProcess); }); spawnProcess.stderr?.on("data", (data) => { // console.log('App.spawnShell.stderr', data) let dataString = outputStringConvert( option.outputEncoding as any, data, ); Log.info("App.spawnShell.stderr", dataString); stderrList.push(dataString); option.stderr?.(dataString, spawnProcess); }); spawnProcess.on("exit", (code, signal) => { // console.log('App.spawnShell.exit', code) Log.info("App.spawnShell.exit", { code, signal }); exitCode = code; if (isWin) { if (0 === code || 1 === code) { isSuccess = true; } } else { if (null === code || 0 === code) { isSuccess = true; } } if (isSuccess) { option.success?.(spawnProcess); } else { option.error?.( `command ${command} failed with code ${code}`, exitCode, spawnProcess, ); } end = true; }); spawnProcess.on("error", (err) => { // console.log('App.spawnShell.error', err) Log.info("App.spawnShell.error", err); option.error?.(err.toString(), -1, spawnProcess); end = true; }); return { stop: () => { Log.info("App.spawnShell.stop"); if (isWin) { _exec( `taskkill /pid ${spawnProcess.pid} /T /F`, { encoding: "binary", }, (err, stdout, stderr) => { if (stdout) { stdout = outputStringConvert( option.outputEncoding as any, stdout, ); } if (stderr) { stderr = outputStringConvert( option.outputEncoding as any, stderr, ); } Log.info( "App.spawnShell.stop.taskkill", JSON.parse(JSON.stringify({ err, stdout, stderr })), ); }, ); } else { spawnProcess.kill("SIGINT"); } }, send: (data) => { Log.info("App.spawnShell.send", data); spawnProcess.stdin.write(data); }, result: async (): Promise => { if (end) { return stdoutList.join("") + stderrList.join(""); } return new Promise((resolve, reject) => { const watchEnd = () => { setTimeout(() => { if (!end) { watchEnd(); return; } if (isSuccess) { resolve(stdoutList.join("") + stderrList.join("")); } else { reject( [ `command ${command} failed with code ${exitCode} : `, stdoutList.join(""), stderrList.join(""), ].join(""), ); } }, 10); }; watchEnd(); }); }, }; }; const spawnBinary = async ( binary: string, args: string[], option: { stdout?: (data: string, process: any) => void; stderr?: (data: string, process: any) => void; success?: (process: any) => void; error?: (msg: string, exitCode: number, process: any) => void; cwd?: string; outputEncoding?: string; env?: Record; shell?: boolean; } | null = null, ): Promise => { args.unshift(extraResolveBin(binary)); const res = await Apps.spawnShell(args, { ...(option || {}), shell: false, }); return await res.result(); }; const availablePortLock: { [port: number]: { lockKey: string; lockTime: number; }; } = {}; /** * 获取一个可用的端口 * @param start 开始的端口 * @param lockKey 锁定的key,避免其他进程获取,默认会创建一个随机的key * @param lockTime 锁定时间,避免在本次获取后未启动服务导致其他进程重复获取 */ const availablePort = async ( start: number, lockKey?: string, lockTime?: number, ): Promise => { lockKey = lockKey || StrUtil.randomString(8); lockTime = lockTime || 60; // expire lock const now = Date.now(); for (const port in availablePortLock) { const lockInfo = availablePortLock[port]; if (lockInfo.lockTime < now) { delete availablePortLock[port]; } } for (let i = start; i < 65535; i++) { const available = await isPortAvailable(i, "0.0.0.0"); const availableLocal = await isPortAvailable(i, "127.0.0.1"); // console.log('isPortAvailable', i, available, availableLocal) if (available && availableLocal) { const lockInfo = availablePortLock[i]; if (lockInfo) { if (lockInfo.lockKey === lockKey) { return i; } else { // other lockKey lock the port continue; } } availablePortLock[i] = { lockKey, lockTime: Date.now() + lockTime * 1000, }; return i; } } throw new Error("no available port"); }; const isPortAvailable = async ( port: number, host?: string, ): Promise => { return new Promise((resolve) => { const server = net.createServer(); server.listen(port, host); server.on("listening", () => { server.close(); resolve(true); }); server.on("error", () => { resolve(false); }); }); }; const fixExecutable = async (executable: string) => { if (isMac || isLinux) { // chmod +x executable await shell(`chmod +x "${executable}"`); } }; const getUserAgent = () => { let param = []; param.push(`AppOpen/${AppConfig.name}/${AppConfig.version}`); param.push( `Platform/${platformName()}/${platformArch()}/${platformVersion()}/${platformUUID()}`, ); return param.join(" "); }; export const Apps = { shell, spawnShell, spawnBinary, availablePort, isPortAvailable, fixExecutable, getUserAgent, }; export default Apps; ================================================ FILE: electron/mapi/app/lib/position.ts ================================================ import { screen } from "electron"; type PositionCache = { x: 0; y: 0; screenWidth: 0; screenHeight: 0; id: -1; }; export const AppPosition = { caches: {} as Record, getCache(name: string): PositionCache { if (!this.caches[name]) { this.caches[name] = { x: 0, y: 0, screenWidth: 0, screenHeight: 0, id: -1, }; } return this.caches[name]; }, get( name: string, calculator?: ( screenX: number, screenY: number, screenWidth: number, screenHeight: number, ) => { x: number; y: number; }, ): { x: number; y: number; } { const cache = this.getCache(name); const { x, y } = screen.getCursorScreenPoint(); const currentDisplay = screen.getDisplayNearestPoint({ x, y }); if (cache.id !== currentDisplay.id) { cache.id = currentDisplay.id; cache.screenWidth = currentDisplay.workArea.width; cache.screenHeight = currentDisplay.workArea.height; if (!calculator) { calculator = ( screenX: number, screenY: number, screenWidth: number, screenHeight: number, ) => { // console.log('calculator', {screenX, screenY, screenWidth, screenHeight}); return { x: screenX + screenWidth / 10, y: screenY + screenHeight / 10, }; }; } const res = calculator( currentDisplay.workArea.x, currentDisplay.workArea.y, cache.screenWidth, cache.screenHeight, ); cache.x = parseInt(String(res.x)); cache.y = parseInt(String(res.y)); } return { x: cache.x, y: cache.y, }; }, set(name: string, x: number, y: number): void { const cache = this.getCache(name); cache.x = x; cache.y = y; }, getContextMenuPosition( boxWidth: number, boxHeight: number, ): { x: number; y: number; } { const { x, y } = screen.getCursorScreenPoint(); const currentDisplay = screen.getDisplayNearestPoint({ x, y }); let resultX = x; let resultY = y; if (currentDisplay.workArea.width - x < boxWidth) { resultX = currentDisplay.workArea.width - boxWidth; } if (currentDisplay.workArea.height - y < boxHeight) { resultY = currentDisplay.workArea.height - boxHeight; } return { x: resultX, y: resultY, }; }, }; ================================================ FILE: electron/mapi/app/loading.ts ================================================ import { BrowserWindow } from "electron"; import { AppsMain } from "./main"; import { icons } from "./icons"; export const makeLoading = ( msg: string, options?: { timeout?: number; percentAuto?: boolean; percentTotalSeconds?: number; }, ): { close: () => void; percent: (value: number) => void; } => { options = Object.assign( { percentAuto: false, percentTotalSeconds: 30, timeout: 0, }, options, ); if (options.timeout === 0) { options.timeout = 60 * 10 * 1000; } // console.log('options', options) const display = AppsMain.getCurrentScreenDisplay(); // console.log('xxxx', primaryDisplay); const width = display.workArea.width; const height = 60; const icon = icons.loading; const win = new BrowserWindow({ height, width, x: 0, y: 0, modal: false, frame: false, alwaysOnTop: true, center: false, transparent: true, hasShadow: false, show: false, focusable: false, skipTaskbar: true, }); const htmlContent = `
${icon}${msg}
`; const encodedHTML = encodeURIComponent(htmlContent); let percentAutoTimer = null; win.loadURL(`data:text/html;charset=UTF-8,${encodedHTML}`); win.on("ready-to-show", async () => { const width = Math.ceil( await win.webContents.executeJavaScript(`(()=>{ const message = document.getElementById('message'); const width = message.scrollWidth; return width; })()`), ); win.setSize(width + 20, height); const x = display.workArea.x + display.workArea.width / 2 - (width + 20) / 2; const y = display.workArea.y + (display.workArea.height * 1) / 4; win.setPosition(Math.floor(x), Math.floor(y)); win.show(); if (options.percentAuto) { let percent = 0; percentAutoTimer = setInterval( () => { percent += 0.01; if (percent >= 1) { clearInterval(percentAutoTimer); return; } controller.percent(percent); }, (options.percentTotalSeconds * 1000) / 100, ); } // win.webContents.openDevTools({ // mode: 'detach' // }) }); const winCloseTimer = setTimeout(() => { win.close(); clearTimeout(winCloseTimer); }, options.timeout); const controller = { close: () => { win.close(); clearTimeout(winCloseTimer); if (percentAutoTimer) { clearInterval(percentAutoTimer); } }, percent: (value: number) => { const percent = 100 * value; win.webContents.executeJavaScript(`(()=>{ const percent = document.querySelector('#percent'); const percentValue = document.querySelector('#percent .value'); percent.style.display = 'block'; percentValue.style.width = '${percent}%'; })()`); }, }; return controller; }; ================================================ FILE: electron/mapi/app/main.ts ================================================ import { app, BrowserWindow, clipboard, ipcMain, nativeImage, nativeTheme, screen, shell, } from "electron"; import { AppConfig } from "../../../src/config"; import { CommonConfig } from "../../config/common"; import { WindowConfig } from "../../config/window"; import { isDev, isMac, platformArch, platformName, platformUUID, platformVersion, } from "../../lib/env"; import { preloadDefault, rendererDistPath } from "../../lib/env-main"; import { Page } from "../../page"; import { ConfigMain } from "../config/main"; import { AppRuntime } from "../env"; import { Events } from "../event/main"; import { Files } from "../file/main"; import Apps from "./index"; import { AppPosition } from "./lib/position"; import { makeLoading } from "./loading"; import { SetupMain } from "./setup"; import { makeToast } from "./toast"; const getWindowByName = (name?: string) => { if (!name || "main" === name) { return AppRuntime.mainWindow; } if ("fastPanel" === name) { return AppRuntime.fastPanelWindow; } return AppRuntime.windows[name]; }; const getCurrentWindow = (window, e) => { let originWindow = BrowserWindow.fromWebContents(e.sender); // if (originWindow !== window) originWindow = detachInstance.getWindow(); return originWindow; }; const quit = () => { app.quit(); }; ipcMain.handle("app:quit", () => { quit(); }); const restart = () => { app.relaunch(); }; ipcMain.handle("app:restart", () => { restart(); }); const windowMin = (name?: string) => { getWindowByName(name)?.minimize(); }; const windowMax = (name?: string) => { const win = getWindowByName(name); if (!win) { return; } if (win.isFullScreen()) { win.setFullScreen(false); win.unmaximize(); win.center(); } else if (win.isMaximized()) { win.unmaximize(); win.center(); } else { win.setMinimumSize(WindowConfig.minWidth, WindowConfig.minHeight); win.maximize(); } }; const windowSetSize = ( name: string | null, width: number, height: number, option?: { includeMinimumSize: boolean; center: boolean; }, ) => { width = parseInt(String(width)); height = parseInt(String(height)); // console.log('windowSetSize', name, width, height, option) const win = getWindowByName(name); if (!win) { return; } option = Object.assign( { includeMinimumSize: true, center: true, }, option, ); if (option.includeMinimumSize) { win.setMinimumSize(width, height); } win.setSize(width, height); if (option.center) { win.center(); } }; ipcMain.handle("app:openExternal", (event, url: string) => { return shell.openExternal(url); }); ipcMain.handle("app:openPath", (event, url: string) => { return shell.openPath(url); }); ipcMain.handle("app:showItemInFolder", (event, url: string) => { return shell.showItemInFolder(url); }); ipcMain.handle("app:getPreload", (event) => { let preload = preloadDefault; if (!preload.startsWith("file://")) { preload = `file://${preload}`; } return preload; }); ipcMain.handle("window:min", (event, name: string) => { windowMin(name); }); ipcMain.handle("window:max", (event, name: string) => { windowMax(name); }); ipcMain.handle( "window:setSize", ( event, name: string | null, width: number, height: number, option?: { includeMinimumSize: boolean; center: boolean; }, ) => { windowSetSize(name, width, height, option); }, ); ipcMain.handle("window:close", (event, name: string) => { getWindowByName(name)?.close(); }); const windowOpen = async ( name: string, option?: { singleton?: boolean; parent?: BrowserWindow; [key: string]: any; }, ) => { name = name || "main"; return Page.open(name, option); }; ipcMain.handle("window:open", (event, name: string, option: any) => { return windowOpen(name, option); }); ipcMain.handle("window:hide", (event, name: string) => { getWindowByName(name)?.hide(); if (isMac) { app.dock.hide(); } }); ipcMain.handle( "window:move", ( event, name: string | null, data: { mouseX: number; mouseY: number; width: number; height: number; }, ) => { const { x, y } = screen.getCursorScreenPoint(); const originWindow = getWindowByName(name); if (!originWindow) return; originWindow.setBounds({ x: x - data.mouseX, y: y - data.mouseY, width: data.width, height: data.height, }); AppPosition.set(name, x - data.mouseX, y - data.mouseY); }, ); const getClipboardText = () => { return clipboard.readText("clipboard"); }; ipcMain.handle("app:getClipboardText", (event) => { return getClipboardText(); }); const setClipboardText = (text: string) => { clipboard.writeText(text, "clipboard"); }; ipcMain.handle("app:setClipboardText", (event, text: string) => { setClipboardText(text); }); const getClipboardImage = () => { const image = clipboard.readImage("clipboard"); return image.isEmpty() ? "" : image.toDataURL(); }; ipcMain.handle("app:getClipboardImage", (event) => { return getClipboardImage(); }); const setClipboardImage = (image: string) => { const img = nativeImage.createFromDataURL(image); clipboard.writeImage(img, "clipboard"); }; ipcMain.handle("app:setClipboardImage", (event, image: string) => { setClipboardImage(image); }); const isDarkMode = () => { if (!CommonConfig.darkModeEnable) { return false; } return nativeTheme.shouldUseDarkColors; }; const shouldDarkMode = async () => { if (!CommonConfig.darkModeEnable) { return false; } const darkMode = (await ConfigMain.get("darkMode")) || "auto"; if ("dark" === darkMode) { return true; } else if ("light" === darkMode) { return false; } else if ("auto" === darkMode) { return isDarkMode(); } return false; }; const defaultDarkModeBackgroundColor = async () => { if (await shouldDarkMode()) { return "#17171A"; } return "#00FFFFFF"; }; nativeTheme.on("updated", () => { Events.broadcast("DarkModeChange", { isDarkMode: isDarkMode() }); AppsMain.defaultDarkModeBackgroundColor().then((color) => { AppRuntime.mainWindow.setBackgroundColor(color); }); }); ipcMain.handle("app:isDarkMode", () => { return isDarkMode(); }); const getCurrentScreenDisplay = () => { const screenPoint = screen.getCursorScreenPoint(); const display = screen.getDisplayNearestPoint(screenPoint); return { bounds: display.bounds, workArea: display.workArea, }; }; const calcPositionInCurrentDisplay = ( position: | "center" | "left-top" | "right-top" | "left-bottom" | "right-bottom", width: number, height: number, ) => { const { bounds, workArea } = getCurrentScreenDisplay(); let x = 0; let y = 0; switch (position) { case "center": x = workArea.x + (workArea.width - width) / 2; y = workArea.y + (workArea.height - height) / 2; break; case "left-top": x = workArea.x; y = workArea.y; break; case "right-top": x = workArea.x + workArea.width - width; y = workArea.y; break; case "left-bottom": x = workArea.x; y = workArea.y + workArea.height - height; break; case "right-bottom": x = workArea.x + workArea.width - width; y = workArea.y + workArea.height - height; break; } return { x: Math.round(x), y: Math.round(y), }; }; const toast = ( msg: string, options?: { duration?: number; status?: "success" | "error" | "info"; }, ) => { return makeToast(msg, options); }; ipcMain.handle("app:toast", (event, msg: string, option?: any) => { return toast(msg, option); }); const loading = ( msg: string, options?: { timeout?: number; percentAuto?: boolean; percentTotalSeconds?: number; }, ): { close: () => void; percent: (value: number) => void; } => { return makeLoading(msg, options); }; ipcMain.handle("app:loading", (event, msg: string, option?: any) => { return loading(msg, option); }); ipcMain.handle("app:setupList", async () => { return SetupMain.list(); }); ipcMain.handle("app:setupOpen", async (event, name: string) => { return SetupMain.open(name); }); const setupIsOk = async () => { return SetupMain.isOk(); }; ipcMain.handle("app:setupIsOk", async () => { return setupIsOk(); }); const getBuildInfo = async () => { if (isDev) { return { buildId: "Development", }; } const json = await Files.read(rendererDistPath("build.json"), { isDataPath: false, }); return JSON.parse(json); }; ipcMain.handle("app:getBuildInfo", async () => { return getBuildInfo(); }); const collect = async (options?: {}) => { return { userAgent: Apps.getUserAgent(), name: AppConfig.name, version: AppConfig.version, uuid: platformUUID(), platformVersion: platformVersion(), platformName: platformName(), platformArch: platformArch(), }; }; ipcMain.handle("app:collect", async (event, options?: {}) => { return collect(options); }); const setAutoLaunch = async (enable: boolean, options?: {}) => { return app.setLoginItemSettings({ openAtLogin: enable, }); }; ipcMain.handle( "app:setAutoLaunch", async (event, enable: boolean, options?: {}) => { return setAutoLaunch(enable, options); }, ); const getAutoLaunch = async (options?: {}) => { return app.getLoginItemSettings().openAtLogin; }; ipcMain.handle("app:getAutoLaunch", async (event, options?: {}) => { return getAutoLaunch(options); }); export default { quit, }; export const AppsMain = { shouldDarkMode, defaultDarkModeBackgroundColor, getWindowByName, getClipboardText, setClipboardText, getClipboardImage, setClipboardImage, getCurrentScreenDisplay, calcPositionInCurrentDisplay, toast, loading, setupIsOk, windowOpen, }; ================================================ FILE: electron/mapi/app/render.ts ================================================ import { ipcRenderer } from "electron"; import { resolve } from "node:path"; import { isPackaged, platformArch, platformName } from "../../lib/env"; import { AppEnv, waitAppEnvReady } from "../env"; import appIndex from "./index"; const isDarkMode = async () => { return ipcRenderer.invoke("app:isDarkMode"); }; const quit = () => { return ipcRenderer.invoke("app:quit"); }; const restart = () => { return ipcRenderer.invoke("app:restart"); }; const isPlatform = (name: "win" | "osx" | "linux") => { return platformName() === name; }; const windowMin = (name?: string) => { return ipcRenderer.invoke("window:min", name); }; const windowMax = (name?: string) => { return ipcRenderer.invoke("window:max", name); }; const windowSetSize = ( name: string | null, width: number, height: number, option?: { includeMinimumSize: boolean; center: boolean; }, ) => { return ipcRenderer.invoke("window:setSize", name, width, height, option); }; const windowOpen = (name: string, option: any) => { return ipcRenderer.invoke("window:open", name, option); }; const windowHide = (name: string) => { return ipcRenderer.invoke("window:hide", name); }; const windowClose = (name: string) => { return ipcRenderer.invoke("window:close", name); }; const windowMove = ( name: string | null, data: { mouseX: number; mouseY: number; width: number; height: number }, ) => { return ipcRenderer.invoke("window:move", name, data); }; const openExternal = (url: string) => { return ipcRenderer.invoke("app:openExternal", url); }; const openPath = (url: string) => { return ipcRenderer.invoke("app:openPath", url); }; const showItemInFolder = (url: string) => { return ipcRenderer.invoke("app:showItemInFolder", url); }; const getPreload = async () => { return ipcRenderer.invoke("app:getPreload"); }; const resourcePathResolve = async (filePath: string) => { await waitAppEnvReady(); const basePath = isPackaged ? process.resourcesPath : AppEnv.appRoot; return resolve(basePath, filePath); }; const extraPathResolve = async (filePath: string) => { await waitAppEnvReady(); const basePath = isPackaged ? process.resourcesPath : "electron/resources"; return resolve(basePath, "extra", filePath); }; const appEnv = async () => { await waitAppEnvReady(); return AppEnv; }; const setRenderAppEnv = (env: any) => { AppEnv.isInit = true; AppEnv.appRoot = env.appRoot; AppEnv.appData = env.appData; AppEnv.userData = env.userData; AppEnv.dataRoot = env.dataRoot; }; const getClipboardText = () => { return ipcRenderer.invoke("app:getClipboardText"); }; const setClipboardText = (text: string) => { return ipcRenderer.invoke("app:setClipboardText", text); }; const getClipboardImage = () => { return ipcRenderer.invoke("app:getClipboardImage"); }; const setClipboardImage = (image: string) => { return ipcRenderer.invoke("app:setClipboardImage", image); }; const toast = (msg: string, option?: any) => { return ipcRenderer.invoke("app:toast", msg, option); }; const setupList = () => { return ipcRenderer.invoke("app:setupList"); }; const setupOpen = (name: string) => { return ipcRenderer.invoke("app:setupOpen", name); }; const setupIsOk = async () => { return ipcRenderer.invoke("app:setupIsOk"); }; const getBuildInfo = async () => { return ipcRenderer.invoke("app:getBuildInfo"); }; const collect = async (options?: {}) => { return ipcRenderer.invoke("app:collect", options); }; const setAutoLaunch = async (enable: boolean, options?: {}) => { return ipcRenderer.invoke("app:setAutoLaunch", enable, options); }; const getAutoLaunch = async (options?: {}) => { return ipcRenderer.invoke("app:getAutoLaunch", options); }; export const AppsRender = { isDarkMode, resourcePathResolve, extraPathResolve, platformName, platformArch, isPlatform, quit, restart, windowMin, windowMax, windowSetSize, windowOpen, windowHide, windowClose, windowMove, openExternal, openPath, showItemInFolder, getPreload, appEnv, setRenderAppEnv, getClipboardText, setClipboardText, getClipboardImage, setClipboardImage, toast, setupList, setupOpen, setupIsOk, getBuildInfo, collect, setAutoLaunch, getAutoLaunch, shell: appIndex.shell, spawnShell: appIndex.spawnShell, spawnBinary: appIndex.spawnBinary, availablePort: appIndex.availablePort, fixExecutable: appIndex.fixExecutable, getUserAgent: appIndex.getUserAgent, }; export default AppsRender; ================================================ FILE: electron/mapi/app/setup.ts ================================================ import { Permissions } from "../../lib/permission"; import { rendererDistPath } from "../../lib/env-main"; export const SetupMain = { async isOk() { if (!(await Permissions.checkAccessibilityAccess())) { return false; } if (!(await Permissions.checkScreenCaptureAccess())) { return false; } return true; }, async list() { return [ { name: "accessibility", title: t("setup.accessibility.title"), status: (await Permissions.checkAccessibilityAccess()) ? "success" : "fail", desc: t("setup.accessibility.desc"), steps: [ { title: t("setup.accessibility.step"), image: rendererDistPath("setup/accessibility.png"), }, ], }, { name: "screen", title: t("setup.screen.title"), status: (await Permissions.checkScreenCaptureAccess()) ? "success" : "fail", desc: t("setup.screen.desc"), steps: [ { title: t("setup.screen.step"), image: rendererDistPath("setup/screen.png"), }, ], }, ]; }, async open(name: string) { switch (name) { case "accessibility": Permissions.askAccessibilityAccess().then(); break; case "screen": Permissions.askScreenCaptureAccess().then(); break; } }, }; ================================================ FILE: electron/mapi/app/toast.ts ================================================ import { BrowserWindow } from "electron"; import { icons } from "./icons"; import { AppsMain } from "./main"; let win = null; let winCloseTimer = null; let winShowTime = null; const toastMsgQueue: { msg: string; options: any }[] = []; export const makeToast = async ( msg: string, options?: { duration?: number; status?: "success" | "error" | "info"; }, ) => { if (win) { if (winShowTime && Date.now() - winShowTime < 1000) { // make previous toast last at least 1 second if (toastMsgQueue.length > 0) { toastMsgQueue.forEach((item) => { item.options = Object.assign({}, item.options, { duration: 1000, }); }); } toastMsgQueue.push({ msg, options }); await new Promise((resolve) => setTimeout(resolve, 1000 - (Date.now() - winShowTime)), ); if (win) { win.close(); } return; } win.close(); } winShowTime = Date.now(); options = Object.assign( { status: "info", duration: 0, }, options, ); if (options.duration === 0) { options.duration = Math.max(msg.length * 400, 3000); } // console.log('toast', msg, options) const display = AppsMain.getCurrentScreenDisplay(); // console.log('xxxx', primaryDisplay); const width = display.workArea.width; const height = 60; const icon = icons[options.status] || icons.success; win = new BrowserWindow({ height, width, parent: null, x: 0, y: 0, modal: false, frame: false, alwaysOnTop: true, // opacity: 0.9, center: false, transparent: true, hasShadow: false, show: false, focusable: false, skipTaskbar: true, }); const htmlContent = `
${icon}${msg}
`; const encodedHTML = encodeURIComponent(htmlContent); win.loadURL(`data:text/html;charset=UTF-8,${encodedHTML}`); win.on("ready-to-show", async () => { if (!win) return; const containerSize = await win.webContents.executeJavaScript(`(()=>{ const message = document.getElementById('message'); const width = message.scrollWidth; const height = message.scrollHeight; return {width:width,height:height}; })()`); // console.log('containerSize', containerSize); const containerWidth = containerSize.width + 20; const containerHeight = containerSize.height + 20; win.setSize(containerWidth, containerHeight); const x = display.workArea.x + display.workArea.width / 2 - containerWidth / 2; const y = display.workArea.y + (display.workArea.height * 1) / 4; win.setPosition(Math.floor(x), Math.floor(y)); win.showInactive(); // win.webContents.openDevTools({ // mode: 'detach' // }) }); win.on("closed", () => { win = null; if (winCloseTimer) { clearTimeout(winCloseTimer); } setTimeout(() => { if (toastMsgQueue.length > 0) { const item = toastMsgQueue.shift(); makeToast(item.msg, item.options); } }, 0); }); winCloseTimer = setTimeout(() => { winCloseTimer = null; if (!win) return; win.close(); }, options.duration); }; ================================================ FILE: electron/mapi/config/index.ts ================================================ import { callHandleFromMainOrRender } from "../env"; const all = async () => { return callHandleFromMainOrRender("config:all"); }; const get = async (key: string, defaultValue: any = null) => { return callHandleFromMainOrRender("config:get", key, defaultValue); }; const set = async (key: string, value: any) => { await callHandleFromMainOrRender("config:set", key, value); }; const allEnv = async () => { return callHandleFromMainOrRender("config:allEnv"); }; const getEnv = async (key: string, defaultValue: any = null) => { return callHandleFromMainOrRender("config:getEnv", key, defaultValue); }; const setEnv = async (key: string, value: any) => { await callHandleFromMainOrRender("config:setEnv", key, value); }; export const ConfigIndex = { all, get, set, allEnv, getEnv, setEnv, }; ================================================ FILE: electron/mapi/config/main.ts ================================================ import path from "node:path"; import { AppEnv } from "../env"; import fs from "node:fs"; import { ipcMain } from "electron"; import { Events } from "../event/main"; let data = null; let dataEnv = {}; const userDataRoot = () => { return path.join(AppEnv.userData, "config.json"); }; const dataRoot = () => { return path.join(AppEnv.dataRoot, "config.json"); }; const filePath = () => { if (fs.existsSync(userDataRoot())) { return userDataRoot(); } return dataRoot(); }; const load = () => { try { let json = fs.readFileSync(filePath()).toString(); json = JSON.parse(json); data = json || {}; } catch (e) { data = {}; } }; const loadIfNeed = () => { if (data === null) { load(); } }; const save = () => { fs.writeFileSync(filePath(), JSON.stringify(data, null, 4)); }; const all = async () => { loadIfNeed(); return data; }; const get = async (key: string, defaultValue: any = null) => { loadIfNeed(); if (!(key in data)) { data[key] = defaultValue; save(); } return data[key]; }; const set = async (key: string, value: any) => { loadIfNeed(); data[key] = value; save(); }; const allEnv = async () => { return dataEnv; }; const getEnv = async (key: string, defaultValue: any = null) => { if (!(key in dataEnv)) { dataEnv[key] = defaultValue; } return dataEnv[key]; }; const setEnv = async (key: string, value: any) => { dataEnv[key] = value; }; ipcMain.handle("config:all", async (_) => { return await all(); }); ipcMain.handle( "config:get", async (_, key: string, defaultValue: any = null) => { return await get(key, defaultValue); }, ); ipcMain.handle("config:set", async (_, key: string, value: any) => { const res = await set(key, value); Events.broadcast("ConfigChange", { key, value }); return res; }); ipcMain.handle("config:allEnv", async (_) => { return await allEnv(); }); ipcMain.handle( "config:getEnv", async (_, key: string, defaultValue: any = null) => { return await getEnv(key, defaultValue); }, ); ipcMain.handle("config:setEnv", async (_, key: string, value: any) => { const res = await setEnv(key, value); Events.broadcast("ConfigEnvChange", { key, value }); return res; }); export const ConfigMain = { all, get, set, allEnv, getEnv, setEnv, }; export default ConfigMain; ================================================ FILE: electron/mapi/config/render.ts ================================================ import { ipcRenderer } from "electron"; const all = async () => { return ipcRenderer.invoke("config:all"); }; const get = async (key: string, defaultValue: any = null) => { return ipcRenderer.invoke("config:get", key, defaultValue); }; const set = async (key: string, value: any) => { return ipcRenderer.invoke("config:set", key, value); }; const allEnv = async () => { return ipcRenderer.invoke("config:allEnv"); }; const getEnv = async (key: string, defaultValue: any = null) => { return ipcRenderer.invoke("config:getEnv", key, defaultValue); }; const setEnv = async (key: string, value: any) => { return ipcRenderer.invoke("config:setEnv", key, value); }; export default { all, get, set, allEnv, getEnv, setEnv, }; ================================================ FILE: electron/mapi/db/db.ts ================================================ ================================================ FILE: electron/mapi/db/main.ts ================================================ import sqlite3, { Database } from "better-sqlite3"; import path from "node:path"; import migration from "./migration"; import { AppEnv } from "../env"; import { Log } from "../log/main"; import { ipcMain } from "electron"; import fs from "node:fs"; import { Files } from "../file/main"; let dbPath: string | null = null; let dbConn: Database | null = null; let dbSuccess = false; const db = { /** * 检查数据库连接是否已初始化 * @throws {string} 如果数据库未初始化则抛出异常 */ _check() { if (!dbSuccess) { throw "DBNotInitialized"; } }, /** * 执行SQL语句(无返回值) * @param {string} sql - SQL语句 * @param {any[]} params - 参数数组 * @returns {Promise} */ async execute(sql: string, params: any = []): Promise { db._check(); try { dbConn.prepare(sql).run(...params); } catch (err) { throw err; } }, /** * 插入数据并返回插入的行ID * @param {string} sql - SQL语句 * @param {any[]} params - 参数数组 * @returns {Promise} 插入的行ID */ async insert(sql: string, params: any = []): Promise { db._check(); try { const result = dbConn.prepare(sql).run(...params); return result.lastInsertRowid; } catch (err) { throw err; } }, /** * 查询单行数据 * @param {string} sql - SQL语句 * @param {any[]} params - 参数数组 * @returns {Promise} 查询结果 */ async first(sql: string, params: any = []): Promise { db._check(); try { return dbConn.prepare(sql).get(...params); } catch (err) { throw err; } }, /** * 查询多行数据 * @param {string} sql - SQL语句 * @param {any[]} params - 参数数组 * @returns {Promise} 查询结果数组 */ async select(sql: string, params: any = []): Promise { db._check(); try { return dbConn.prepare(sql).all(...params); } catch (err) { throw err; } }, /** * 更新数据并返回影响的行数 * @param {string} sql - SQL语句 * @param {any[]} params - 参数数组 * @returns {Promise} 影响的行数 */ async update(sql: string, params: any = []): Promise { db._check(); try { const result = dbConn.prepare(sql).run(...params); return result.changes; } catch (err) { throw err; } }, /** * 删除数据并返回影响的行数 * @param {string} sql - SQL语句 * @param {any[]} params - 参数数组 * @returns {Promise} 影响的行数 */ async delete(sql: string, params: any = []): Promise { db._check(); try { const result = dbConn.prepare(sql).run(...params); return result.changes; } catch (err) { throw err; } }, }; const migrate = async () => { await db.execute(`CREATE TABLE IF NOT EXISTS migrate ( id INTEGER PRIMARY KEY, version INTEGER )`); for (const version of migration.versions) { const result = await db.first( `SELECT * FROM migrate WHERE version = ?`, [version.version], ); if (!result) { Log.info(`DB.Migrate`, { version: version.version }); await version.up(db); await db.execute( `INSERT INTO migrate (version) VALUES (?)`, [version.version], ); } } }; /** * 初始化数据库连接 * @returns {Promise} */ const init = async () => { dbPath = path.join(AppEnv.dataRoot, "database.db"); const userDbPath = path.join(AppEnv.userData, "database.db"); if (fs.existsSync(userDbPath)) { dbPath = userDbPath; } try { dbConn = new sqlite3(dbPath); dbSuccess = true; await migrate(); Log.info("Database connected successfully"); } catch (err) { Log.error("DBConnect SQLite database failed:", err.message); throw err; } }; ipcMain.handle("db:execute", (event, sql: string, params: any) => { return db.execute(sql, params); }); ipcMain.handle("db:insert", (event, sql: string, params: any) => { return db.insert(sql, params); }); ipcMain.handle("db:first", (event, sql: string, params: any) => { return db.first(sql, params); }); ipcMain.handle("db:select", (event, sql: string, params: any) => { return db.select(sql, params); }); ipcMain.handle("db:update", (event, sql: string, params: any) => { return db.update(sql, params); }); ipcMain.handle("db:delete", (event, sql: string, params: any) => { return db.delete(sql, params); }); export const DBMain = { init, execute: db.execute, insert: db.insert, first: db.first, select: db.select, update: db.update, delete: db.delete, }; export default DBMain; ================================================ FILE: electron/mapi/db/migration.ts ================================================ const versions = [ { version: 0, up: async (db: DB) => { // await db.execute(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`); // console.log('db.insert', await db.insert(`INSERT INTO users (name, email) VALUES (?, ?)`,['Alice', 'alice@example.com'])); // console.log('db.select', await db.select(`SELECT * FROM users`)); // console.log('db.first', await db.first(`SELECT * FROM users`)); }, }, { version: 1, up: async (db: DB) => { await db.execute(`CREATE TABLE IF NOT EXISTS kvdb_data ( id TEXT PRIMARY KEY, cloudVersion INTEGER, version INTEGER, isDeleted INTEGER, name TEXT )`); await db.execute(`CREATE INDEX IF NOT EXISTS idx_kvdb_data_name ON kvdb_data (name) `); }, }, ]; export default { versions, }; ================================================ FILE: electron/mapi/db/render.ts ================================================ import { ipcRenderer } from "electron"; const init = () => {}; const execute = async (sql: string, params: any = []) => { return ipcRenderer.invoke("db:execute", sql, params); }; const insert = async (sql: string, params: any = []) => { return ipcRenderer.invoke("db:insert", sql, params); }; const first = async (sql: string, params: any = []) => { return ipcRenderer.invoke("db:first", sql, params); }; const select = async (sql: string, params: any = []) => { return ipcRenderer.invoke("db:select", sql, params); }; const update = async (sql: string, params: any = []) => { return ipcRenderer.invoke("db:update", sql, params); }; const deletes = async (sql: string, params: any = []) => { return ipcRenderer.invoke("db:delete", sql, params); }; export default { init, execute, insert, first, select, update, delete: deletes, }; ================================================ FILE: electron/mapi/db/type.d.ts ================================================ type DB = { execute(sql: string, params?: any): Promise; insert(sql: string, params?: any): Promise; first(sql: string, params?: any): Promise; select(sql: string, params?: any): Promise; update(sql: string, params?: any): Promise; delete(sql: string, params?: any): Promise; }; ================================================ FILE: electron/mapi/env.ts ================================================ import electron, { BrowserWindow } from "electron"; import { Log } from "./log"; export const AppEnv = { isInit: false, appRoot: null as string, appData: null as string, userData: null as string, dataRoot: null as string, }; export const AppRuntime = { fileHubRoot: null as string, splashWindow: null as BrowserWindow, mainWindow: null as BrowserWindow, fastPanelWindow: null as BrowserWindow, windows: {} as Record, }; export const waitAppEnvReady = async () => { while (!AppEnv.isInit) { await new Promise((resolve) => { setTimeout(resolve, 1000); }); } }; export const callHandleFromMainOrRender = async (name: string, ...args) => { if (electron.ipcRenderer) { return electron.ipcRenderer.invoke(name, ...args); } else { // @ts-ignore const func = electron.ipcMain._invokeHandlers.get(name); if (func) { return func(...args); } else { Log.error(`No handler found for ${name}`); return null; } } }; ================================================ FILE: electron/mapi/event/main.ts ================================================ import { AppRuntime } from "../env"; import { ipcMain, WebContents } from "electron"; import { StrUtil } from "../../lib/util"; import { ManagerWindow } from "../manager/window"; const init = async () => {}; type NameType = "main" | "fastPanel" | string | WebContents; type EventType = "APP_READY" | "CALL_PAGE" | "CHANNEL" | "BROADCAST"; type BroadcastType = | "ConfigChange" | "ConfigEnvChange" | "UserChange" | "DarkModeChange" | "HotkeyWatch" | "Notice" | "MonitorEvent"; const broadcast = ( type: BroadcastType, data: any, option?: { limit?: boolean; scopes?: string[]; pages?: string[]; }, ) => { data = data || {}; option = Object.assign( { limit: false, scopes: [], pages: [], }, option, ); if (option.pages.length > 0) { for (const p of option.pages) { send(p, "BROADCAST", { type, data }); } } else { if (!option.limit || option.scopes.includes("main")) { send("main", "BROADCAST", { type, data }); } if (!option.limit || option.scopes.includes("pages")) { for (let name in AppRuntime.windows) { send(name, "BROADCAST", { type, data }); } } } if (!option.limit || option.scopes.includes("fastPanel")) { send("fastPanel", "BROADCAST", { type, data }); } if (!option.limit || option.scopes.includes("views")) { for (const view of ManagerWindow.listBrowserViews()) { view.webContents.send("MAIN_PROCESS_MESSAGE", { id: StrUtil.randomString(32), type: "BROADCAST", data: { type, data }, }); } } if (!option.limit || option.scopes.includes("detachWindows")) { for (const win of ManagerWindow.listDetachWindows()) { win.webContents.send("MAIN_PROCESS_MESSAGE", { id: StrUtil.randomString(32), type: "BROADCAST", data: { type, data }, }); } } }; const sendRaw = ( webContents: any, type: EventType, data: any = {}, id?: string, ): boolean => { id = id || StrUtil.randomString(32); const payload = { id, type, data }; webContents.send("MAIN_PROCESS_MESSAGE", payload); return true; }; const send = ( name: NameType, type: EventType, data: any = {}, id?: string, ): boolean => { id = id || StrUtil.randomString(32); const payload = { id, type, data }; if (typeof name !== "string") { (name as WebContents).send("MAIN_PROCESS_MESSAGE", payload); return true; } if (name === "main") { if (!AppRuntime.mainWindow) { return false; } // console.log('send', payload) AppRuntime.mainWindow?.webContents.send( "MAIN_PROCESS_MESSAGE", payload, ); } else if (name === "fastPanel") { if (!AppRuntime.fastPanelWindow) { return false; } AppRuntime.fastPanelWindow?.webContents.send( "MAIN_PROCESS_MESSAGE", payload, ); } else { if (!AppRuntime.windows[name]) { return false; } AppRuntime.windows[name]?.webContents.send( "MAIN_PROCESS_MESSAGE", payload, ); } return true; }; ipcMain.handle( "event:send", async (_, name: NameType, type: EventType, data: any) => { send(name, type, data); }, ); const callPage = async ( name: NameType, type: string, data: any, option?: { waitReadyTimeout?: number; timeout?: number; }, ): Promise<{ code: number; msg: string; data?: any; }> => { option = Object.assign( { waitReadyTimeout: 10 * 1000, timeout: 60 * 1000, }, option, ); return new Promise((resolve, reject) => { const id = StrUtil.randomString(32); const timer = setTimeout(() => { ipcMain.removeListener(listenerKey, listener); resolve({ code: -1, msg: "timeout" }); }, option.timeout); const listener = (_, result) => { clearTimeout(timer); resolve(result); return true; }; const listenerKey = "event:callPage:" + id; ipcMain.once(listenerKey, listener); const payload = { type, data, option: { waitReadyTimeout: option.waitReadyTimeout, }, }; if (!send(name, "CALL_PAGE", payload, id)) { clearTimeout(timer); ipcMain.removeListener(listenerKey, listener); resolve({ code: -1, msg: "send failed" }); } }); }; ipcMain.handle( "event:callPage", async (_, name: string, type: string, data: any, option?: {}) => { return callPage(name, type, data, option); }, ); let onChannelIsListen = false; let channelOnCallback = {}; const sendChannel = (channel: string, data: any) => { send("main", "CHANNEL", { channel, data }); }; const onChannel = (channel: string, callback: (data: any) => void) => { if (!channelOnCallback[channel]) { channelOnCallback[channel] = []; } channelOnCallback[channel].push(callback); if (!onChannelIsListen) { onChannelIsListen = true; ipcMain.handle("event:channelSend", (event, channel_, data) => { if (channelOnCallback[channel_]) { channelOnCallback[channel_].forEach( (callback: (data: any) => void) => { callback(data); }, ); } }); } }; const offChannel = (channel: string, callback: (data: any) => void) => { if (channelOnCallback[channel]) { channelOnCallback[channel] = channelOnCallback[channel].filter( (item: (data: any) => void) => { return item !== callback; }, ); } if (channelOnCallback[channel].length === 0) { delete channelOnCallback[channel]; } }; export default { init, send, }; export const Events = { broadcast, send, sendRaw, sendChannel, callPage, onChannel, offChannel, }; ================================================ FILE: electron/mapi/event/render.ts ================================================ import { ipcRenderer } from "electron"; const init = () => {}; const send = (name: string, type: string, data: any = {}) => { return ipcRenderer.invoke("event:send", name, type, data).then(); }; const callPage = async (name: string, type: string, data: any, option: any) => { return ipcRenderer.invoke("event:callPage", name, type, data, option); }; const channelSend = async (channel: string, data: any) => { return ipcRenderer.invoke("event:channelSend", channel, data); }; export default { init, send, callPage, channelSend, }; ================================================ FILE: electron/mapi/file/index.ts ================================================ import fs, { createWriteStream } from "node:fs"; import path from "node:path"; import { Readable } from "node:stream"; import { ReadableStream } from "node:stream/web"; import { EncodeUtil, StrUtil, TimeUtil } from "../../lib/util"; import Apps from "../app"; import { ConfigIndex } from "../config"; import { AppEnv, waitAppEnvReady } from "../env"; import { Log } from "../log"; import electron from "electron"; import { finished } from "stream/promises"; const nodePath = path; const toNodeReadableStream = (stream: any) => { if (stream instanceof ReadableStream) { // 已经是 Node.js 版本的 WHATWG ReadableStream return Readable.fromWeb(stream); } if (typeof stream.getReader === "function") { // 浏览器版本 → 包装成 Node.js 兼容的 const nodeStream = new ReadableStream({ async pull(controller) { const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; controller.enqueue(value); } controller.close(); }, }); return Readable.fromWeb(nodeStream); } throw new Error("Unsupported stream type"); }; const toWebReadableStream = (stream: any) => { const reader = stream[Symbol.asyncIterator](); return new window.ReadableStream({ async pull(controller) { const { value, done } = await reader.next(); if (done) { controller.close(); } else { controller.enqueue(value); } }, }); }; const root = () => { return AppEnv.dataRoot; }; const absolutePath = (path: string) => { return `ABS://${path}`; }; const fullPath = async (path: string) => { await waitAppEnvReady(); if (path.startsWith("ABS://")) { return path.replace(/^ABS:\/\//, ""); } return nodePath.join(root(), path); }; const exists = async ( path: string, option?: { isDataPath?: boolean }, ): Promise => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } return new Promise((resolve, reject) => { fs.stat(fp, (err, stat) => { if (err) { resolve(false); } else { resolve(true); } }); }); }; const isDirectory = async (path: string, option?: { isDataPath?: boolean }) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { return false; } return fs.statSync(fp).isDirectory(); }; const mkdir = async (path: string, option?: { isDataPath?: boolean }) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { fs.mkdirSync(fp, { recursive: true }); } }; const list = async (path: string, option?: { isDataPath?: boolean }) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { return []; } const files = fs.readdirSync(fp); return files.map((file) => { const stat = fs.statSync(nodePath.join(fp, file)); let f = { name: file, pathname: nodePath.join(fp, file), isDirectory: stat.isDirectory(), size: stat.size, lastModified: stat.mtimeMs, }; return f; }); }; const listAll = async (path: string, option?: { isDataPath?: boolean }) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { return []; } const listDirectory = (path: string, basePath: string = "") => { let files = []; const list = fs.readdirSync(path); for (let file of list) { const stat = fs.statSync(nodePath.join(path, file)); let fPath = nodePath.join(basePath, file); fPath = fPath.replace(/\\/g, "/"); let f = { name: file, path: fPath, isDirectory: stat.isDirectory(), size: stat.size, lastModified: stat.mtimeMs, }; if (f.isDirectory) { files = files.concat( listDirectory(nodePath.join(path, file), f.path), ); continue; } files.push(f); } return files; }; return listDirectory(fp); }; const write = async ( path: string, data: any, option?: { isDataPath?: boolean }, ) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } const fullPathDir = nodePath.dirname(fp); if (!fs.existsSync(fullPathDir)) { fs.mkdirSync(fullPathDir, { recursive: true }); } if (typeof data === "string") { data = { content: data, }; } const f = fs.openSync(fp, "w"); fs.writeSync(f, data.content); fs.closeSync(f); }; const writeStream = async ( path: string, data: any, option?: { isDataPath?: boolean }, ) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } const fullPathDir = nodePath.dirname(fp); if (!fs.existsSync(fullPathDir)) { fs.mkdirSync(fullPathDir, { recursive: true }); } if (electron.ipcRenderer) { data = toNodeReadableStream(data); } const fileStream = createWriteStream(fp); data.pipe(fileStream); await finished(fileStream); }; const writeBuffer = async ( path: string, data: any, option?: { isDataPath?: boolean }, ) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } const fullPathDir = nodePath.dirname(fp); if (!fs.existsSync(fullPathDir)) { fs.mkdirSync(fullPathDir, { recursive: true }); } const f = fs.openSync(fp, "w"); fs.writeSync(f, data); fs.closeSync(f); }; const read = async ( path: string, option?: { isDataPath?: boolean; encoding?: string; }, ) => { option = Object.assign( { isDataPath: false, encoding: "utf8", }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { return null; } const f = fs.openSync(fp, "r"); const content = fs.readFileSync(f, { encoding: option.encoding as BufferEncoding, }); fs.closeSync(f); return content; }; const readBuffer = async ( path: string, option?: { isDataPath?: boolean }, ): Promise => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { return null; } return new Promise((resolve, reject) => { fs.readFile(fp, (err, data) => { if (err) { reject(err); return; } resolve(data); }); }); }; const readStream = async (path: string, option?: { isDataPath?: boolean }) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { throw `FileNotFound: ${fp}`; } const stream = fs.createReadStream(fp); if (electron.ipcRenderer) { return toWebReadableStream(stream); } return stream; }; const readLine = async ( path: string, callback: (line: string) => void, option?: { isDataPath?: boolean; }, ) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!fs.existsSync(fp)) { return; } return new Promise((resolve, reject) => { const f = fs.createReadStream(fp); let remaining = ""; f.on("data", (chunk) => { remaining += chunk; let index = remaining.indexOf("\n"); let last = 0; while (index > -1) { let line = remaining.substring(last, index); last = index + 1; callback(line); index = remaining.indexOf("\n", last); } remaining = remaining.substring(last); }); f.on("end", () => { if (remaining.length > 0) { callback(remaining); } resolve(undefined); }); }); }; const clean = async (paths: string[], option?: { isDataPath?: boolean }) => { if (!paths || !Array.isArray(paths) || paths.length === 0) { return; } for (const path of paths) { try { await deletes(path, option); } catch (e) { Log.error(`CleanError: ${path}`, e); } } }; const deletes = async ( path: string, option?: { isDataPath?: boolean }, ): Promise => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (!(await exists(fp, { isDataPath: false }))) { return; } return new Promise((resolve, reject) => { fs.stat(fp, (err, stat) => { if (err) { reject(err); return; } if (stat.isDirectory()) { fs.rmdir(fp, { recursive: true }, (err) => { if (err) { reject(err); return; } resolve(undefined); }); } else { fs.unlink(fp, (err) => { if (err) { reject(err); return; } resolve(undefined); }); } }); }); }; const rename = async ( pathOld: string, pathNew: string, option?: { isDataPath?: boolean; overwrite?: boolean; }, ) => { option = Object.assign( { isDataPath: false, overwrite: false, }, option, ); let fullPathOld = pathOld; let fullPathNew = pathNew; if (option.isDataPath) { fullPathOld = await fullPath(pathOld); fullPathNew = await fullPath(pathNew); } if (!fs.existsSync(fullPathOld)) { throw `Rename.FileNotFound - ${fullPathOld}`; } if (fs.existsSync(fullPathNew)) { if (!option.overwrite) { throw new Error(`FileAlreadyExists:${fullPathNew}`); } fs.unlinkSync(fullPathNew); } const dir = nodePath.dirname(fullPathNew); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } let success = false; try { fs.renameSync(fullPathOld, fullPathNew); success = true; } catch (e) {} if (!success) { // cross-device link not permitted, rename fs.copyFileSync(fullPathOld, fullPathNew); fs.unlinkSync(fullPathOld); } }; const copy = async ( pathOld: string, pathNew: string, option?: { isDataPath?: boolean; overwrite?: boolean; }, ) => { option = Object.assign( { isDataPath: false, overwrite: false, }, option, ); let fullPathOld = pathOld; let fullPathNew = pathNew; if (option.isDataPath) { fullPathOld = await fullPath(pathOld); fullPathNew = await fullPath(pathNew); } if (!fs.existsSync(fullPathOld)) { throw `Copy.FileNotFound - ${fullPathOld}`; } if (fs.existsSync(fullPathNew)) { if (option.overwrite) { await deletes(fullPathNew, { isDataPath: false }); } else { throw `Copy.FileAlreadyExists - ${fullPathNew}`; } } // console.log('copy', fullPathOld, fullPathNew) const dir = nodePath.dirname(fullPathNew); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.copyFileSync(fullPathOld, fullPathNew); }; const hubRootDefault = async () => { await waitAppEnvReady(); return path.join(root(), "hub"); }; const hubRoot = async (): Promise => { const hubDirDefault = await hubRootDefault(); let hubDir = await ConfigIndex.get("hubRoot", ""); if (!hubDir) { hubDir = hubDirDefault; } if (!fs.existsSync(hubDir)) { fs.mkdirSync(hubDir, { recursive: true }); } return hubDir; }; const _getHubSavePath = async ( hubRoot: string, saveGroup: string, savePath: string, saveParam: { [key: string]: any; }, ext: string, autoCreateDir: boolean = false, ) => { if (!saveGroup) { saveGroup = "file"; } if (!savePath) { savePath = path.join( saveGroup, "{year}{month}{day}", "{hour}{minute}_{second}_{random}", ); } savePath = savePath.replace(/\\/g, "/"); if (savePath.endsWith(`.${ext}`)) { savePath = savePath.substring(0, savePath.length - ext.length - 1); } for (const key in saveParam) { // only allow alphanumeric, Chinese characters, and hyphens saveParam[key] = saveParam[key] .toString() .replace(/[^\w\u4e00-\u9fa5\-]/g, ""); // length limit if (saveParam[key].length > 100) { saveParam[key] = saveParam[key].substring(0, 100); } } const param = { year: TimeUtil.replacePattern("{year}"), month: TimeUtil.replacePattern("{month}"), day: TimeUtil.replacePattern("{day}"), hour: TimeUtil.replacePattern("{hour}"), minute: TimeUtil.replacePattern("{minute}"), second: TimeUtil.replacePattern("{second}"), random: StrUtil.randomString(32), ...saveParam, }; savePath = savePath.replace(/\{(\w+)\}/g, (match, key) => { return param[key] || key; }); while ( await exists(path.join(hubRoot, savePath + `.${ext}`), { isDataPath: false, }) ) { savePath = savePath + `-${StrUtil.randomString(3)}`; } if (autoCreateDir) { const savePathFull = path.join(hubRoot, savePath); const dir = nodePath.dirname(savePathFull); if (!(await exists(dir, { isDataPath: false }))) { fs.mkdirSync(dir, { recursive: true }); } } return `${savePath}.${ext}`; }; const hubDelete = async ( file: string, option?: { isDataPath?: boolean; ignoreWhenNotInHub?: boolean; tryLaterWhenFailed?: boolean; }, ) => { option = Object.assign( { isDataPath: false, ignoreWhenNotInHub: true, tryLaterWhenFailed: true, }, option, ); let fp = file; const hubRoot_ = await hubRoot(); if (option.isDataPath) { fp = path.join(hubRoot_, file); } if (!(await isHubFile(fp))) { if (option.ignoreWhenNotInHub) { return; } } if (!(await exists(fp, { isDataPath: false }))) { throw `HubDelete.FileNotFound - ${fp}`; } const del = () => { deletes(fp, { isDataPath: false }).catch((err) => { if (option.tryLaterWhenFailed) { setTimeout(del, 1000); } else { Log.error(`HubDelete.Error: ${fp}`, err); } }); }; del(); }; const hubFullPath = async (file: string): Promise => { if (!file) { throw "HubSave.FilePathEmpty"; } const hubRoot_ = await hubRoot(); return path.join(hubRoot_, file); }; const hubFile = async ( ext: string, option?: { returnFullPath?: boolean; autoCreateDir?: boolean; saveGroup?: string; savePath?: string; savePathParam?: { [key: string]: any; }; }, ) => { option = Object.assign( { returnFullPath: true, autoCreateDir: true, saveGroup: "file", savePath: null, savePathParam: {}, }, option, ); if (!ext) { throw "HubSave.FilePathEmpty"; } const hubRoot_ = await hubRoot(); const savePath = await _getHubSavePath( hubRoot_, option.saveGroup, option.savePath, option.savePathParam, ext, option.autoCreateDir, ); if (option.returnFullPath) { return path.join(hubRoot_, savePath); } return savePath; }; const isHubFile = async (file: string) => { const hubRoot_ = await hubRoot(); return inDir(file, hubRoot_); }; const hubSave = async ( file: string, option?: { ext?: string; returnFullPath?: boolean; ignoreWhenInHub?: boolean; cleanOld?: boolean; saveGroup?: string; savePath?: string; savePathParam?: { [key: string]: any; }; }, ) => { option = Object.assign( { ext: null, returnFullPath: true, ignoreWhenInHub: false, cleanOld: false, saveGroup: "file", savePath: null, savePathParam: {}, }, option, ); if (!file) { throw "HubSave.FilePathEmpty"; } if (!fs.existsSync(file)) { throw `HubSave.FileNotFound - ${file}`; } if (!option.ext) { option.ext = ext(file); } const hubRoot_ = await hubRoot(); if (option.ignoreWhenInHub) { if (inDir(file, hubRoot_)) { return file; } } const savePath = await _getHubSavePath( hubRoot_, option.saveGroup, option.savePath, option.savePathParam, option.ext, ); const savePathFull = path.join(hubRoot_, savePath); if (option.cleanOld) { await rename(file, savePathFull, { isDataPath: false }); if (await exists(file, { isDataPath: false })) { deletes(file, { isDataPath: false }).then(); } } else { await copy(file, savePathFull, { isDataPath: false, }); } if (option.returnFullPath) { return savePathFull; } return savePath; }; const hubSaveContent = async ( content: string, option: { ext: string; returnFullPath?: boolean; saveGroup?: string; savePath?: string; savePathParam?: { [key: string]: any; }; }, ) => { option = Object.assign( { ext: null, returnFullPath: true, saveGroup: "file", savePath: null, savePathParam: {}, }, option, ); const hubRoot_ = await hubRoot(); const savePath = await _getHubSavePath( hubRoot_, option.saveGroup, option.savePath, option.savePathParam, option.ext, ); const savePathFull = path.join(hubRoot_, savePath); await write(savePathFull, content, { isDataPath: false }); if (option.returnFullPath) { return savePathFull; } return savePath; }; const tempRoot = async () => { await waitAppEnvReady(); const tempDir = path.join(root(), "temp"); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } return tempDir; }; const autoCleanTemp = async (keepDays: number = 7) => { const root = await tempRoot(); if (!fs.existsSync(root)) { return; } const files = fs.readdirSync(root); const now = new Date(); for (const file of files) { const filePath = path.join(root, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { continue; // skip directories } const lastModified = new Date(stat.mtimeMs); const diffDays = Math.floor( (now.getTime() - lastModified.getTime()) / (1000 * 60 * 60 * 24), ); if (diffDays >= keepDays) { fs.unlinkSync(filePath); Log.info("AutoCleanTemp.Clean", filePath); } else { // console.log('AutoCleanTemp.Skip', filePath, diffDays); } } }; const tempName = async ( ext: string = "tmp", prefix: string = "file", suffix: string = "", ): Promise => { const parts = [prefix, TimeUtil.timestampInMs(), StrUtil.randomString(32)]; if (suffix) { parts.push(suffix); } const p = parts.join("_"); return `${p}.${ext}`; }; const temp = async ( ext: string = "tmp", prefix: string = "file", suffix: string = "", ): Promise => { const root = await tempRoot(); return path.join(root, await tempName(ext, prefix, suffix)); }; const tempDir = async (prefix: string = "dir"): Promise => { const root = await tempRoot(); const p = [prefix, TimeUtil.timestampInMs(), StrUtil.randomString(32)].join( "_", ); const dir = path.join(root, p); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } return dir; }; const watchText = async ( path: string, callback: (data: {}) => void, option?: { isDataPath?: boolean; limit?: number; }, ): Promise<{ stop: Function; }> => { if (!path) { throw new Error("path is empty"); } option = Object.assign( { isDataPath: false, limit: 0, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } let watcher = null; let fd = null; let isFirstReading = true; let firstReadingLines = []; const watchFileExists = () => { if (fs.existsSync(fp)) { watcher = null; watchFileContent(); return; } watcher = setTimeout(() => { watchFileExists(); }, 1000); }; const watchFileContent = () => { const CHUNK_SIZE = 16 * 1024; const fd = fs.openSync(fp, "r"); let position = 0; let lineNumber = 0; let content = ""; const parseContentLine = () => { while (true) { const index = content.indexOf("\n"); if (index < 0) { break; } const line = content.substring(0, index); content = content.substring(index + 1); const lineItem = { num: lineNumber++, text: line, }; if (option.limit > 0 && isFirstReading) { // 限制显示模式并且是第一次读取,暂时先不回调 firstReadingLines.push(lineItem); while (firstReadingLines.length >= option.limit) { firstReadingLines.shift(); } } else { callback(lineItem); } // console.log('watchText.line', line, content) } }; const readChunk = () => { const buf = new Buffer(CHUNK_SIZE); const bytesRead = fs.readSync(fd, buf, 0, CHUNK_SIZE, position); position += bytesRead; content += buf.toString("utf8", 0, bytesRead); parseContentLine(); if (bytesRead < CHUNK_SIZE) { isFirstReading = false; if (firstReadingLines.length > 0) { firstReadingLines.forEach((lineItem) => { callback(lineItem); }); firstReadingLines = []; } watcher = setTimeout(readChunk, 1000); } else { readChunk(); } }; readChunk(); }; watchFileExists(); const stop = () => { // console.log('watchText stop', fp) if (fd) { fs.closeSync(fd); } if (watcher) { clearTimeout(watcher); } }; // console.log('watchText', fp) return { stop, }; }; let appendTextPathCached = null; let appendTextStreamCached = null; const appendText = async ( path: string, data: any, option?: { isDataPath?: boolean }, ) => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } if (fp !== appendTextPathCached) { appendTextPathCached = fp; if (appendTextStreamCached) { appendTextStreamCached.end(); appendTextStreamCached = null; } const fullPathDir = nodePath.dirname(fp); if (!fs.existsSync(fullPathDir)) { fs.mkdirSync(fullPathDir, { recursive: true }); } appendTextStreamCached = fs.createWriteStream(fp, { flags: "a" }); } appendTextStreamCached.write(data); }; const download = async ( url: string, path: string | null = null, option?: { isDataPath?: boolean; userAgent?: string; progress?: (percent: number, total: number) => void; }, ): Promise => { option = Object.assign( { isDataPath: false, userAgent: Apps.getUserAgent(), progress: null, }, option, ); if (!path) { const ext = FileIndex.ext(url); path = await temp(ext || "bin", "download"); option.isDataPath = false; } let fp = path; if (option.isDataPath) { fp = await fullPath(path); } const fullPathDir = nodePath.dirname(fp); if (!fs.existsSync(fullPathDir)) { fs.mkdirSync(fullPathDir, { recursive: true }); } const res = await fetch(url, { method: "GET", headers: { "User-Agent": option.userAgent, }, }); if (!res.ok) { throw new Error(`DownloadError:${url}`); } const contentLength = res.headers.get("content-length"); const totalSize = contentLength ? parseInt(contentLength, 10) : null; let downloaded = 0; let readableStream = toNodeReadableStream(res.body); const fileStream = fs.createWriteStream(fp); return new Promise((resolve, reject) => { readableStream .on("data", (chunk) => { // console.log('download.data', chunk.length) downloaded += chunk.length; if (totalSize) { option.progress && option.progress(downloaded / totalSize, totalSize); } fileStream.write(chunk); }) .on("end", () => { // console.log('download.end') fileStream.end(); resolve(fp); }) .on("error", (err) => { // console.log('download.error', err) fileStream.close(); reject(err); }); }); }; /** * get file extension from file path or url * @param path */ const ext = (path: string) => { if (!path) { return ""; } if (path.startsWith("http://") || path.startsWith("https://")) { // 处理 URL const url = new URL(path); path = url.pathname; } return nodePath.extname(path).replace(/^\./, ""); }; const stat = async ( path: string, option?: { isDataPath?: boolean }, ): Promise<{ size: number; isDirectory: boolean; lastModified: number; }> => { option = Object.assign( { isDataPath: false, }, option, ); let fp = path; if (option.isDataPath) { fp = await fullPath(path); } const stat = fs.statSync(fp); return { size: stat.size, isDirectory: stat.isDirectory(), lastModified: stat.mtimeMs, }; }; const textToName = (text: string, ext: string = "", maxLimit: number = 100) => { if (text) { // 转换为合法的文件名 text = text.replace(/[\\\/\:\*\?\"\<\>\|]/g, ""); text = text.replace(/[\r\n]/g, ""); text = text.replace(/\s+/g, ""); text = text.substring(0, maxLimit); } if (!text) { text = "EMPTY"; } if (!ext) { return text; } return `${text}.${ext}`; }; const inDir = (path: string, dir: string) => { if (!path || !dir) { return false; } path = path.replace(/\\/g, "/"); dir = dir.replace(/\\/g, "/"); if (path === dir) { return true; } return path.startsWith(dir); }; const pathToName = ( path: string, includeExt: boolean = true, maxLimit: number = 100, ) => { if (!path) { return ""; } path = path.replace(/\\/g, "/"); const parts = path.split("/"); const nameWithExt = parts[parts.length - 1]; const nameParts = nameWithExt.split("."); let ext = ""; if (nameParts.length > 1) { ext = "." + nameParts.pop(); } if (!includeExt) { ext = ""; } let result = nameParts.join("."); maxLimit -= ext.length; if (maxLimit > 0 && result.length > maxLimit) { result = result.substring(0, maxLimit); } if (!result) { result = "EMPTY"; } return `${result}${ext}`; }; const _sortObjectDeep = (obj: any): any => { if (Array.isArray(obj)) { return obj.map(_sortObjectDeep); } else if (obj && typeof obj === "object") { return Object.keys(obj) .sort() .reduce((acc, key) => { acc[key] = _sortObjectDeep(obj[key]); return acc; }, {} as any); } return obj; }; const cacheKey = async (key: any): Promise => { const keyObjString = JSON.stringify(_sortObjectDeep(key)); const keyMd5 = EncodeUtil.md5(keyObjString); return path.join(await tempRoot(), `FileCache_${keyMd5}`); }; const cacheForget = async (key: any): Promise => { const keyPath = await cacheKey(key); if (await exists(keyPath)) { await deletes(keyPath); } }; const cacheSet = async (key: any, data: any): Promise => { const keyPath = await cacheKey(key); await write(keyPath, JSON.stringify(data)); }; const cacheGet = async (key: any): Promise => { const keyPath = await cacheKey(key); if (!(await exists(keyPath))) { return null; } const content = await read(keyPath); if (!content) { return null; } try { return JSON.parse(content); } catch (e) { return null; } }; const cacheGetPath = async (key: any): Promise => { const p = await cacheGet(key); if (!p) { return null; } if (!(await exists(p))) { await cacheForget(key); return null; } return p; }; export const FileIndex = { fullPath, absolutePath, exists, isDirectory, mkdir, list, listAll, write, writeStream, writeBuffer, read, readBuffer, readStream, readLine, clean, deletes, rename, copy, tempRoot, tempName, temp, tempDir, watchText, appendText, download, ext, stat, textToName, pathToName, hubRootDefault, hubRoot, hubSave, hubSaveContent, hubDelete, hubFile, hubFullPath, isHubFile, cacheForget, cacheSet, cacheGetPath, cacheGet, autoCleanTemp, }; export default FileIndex; ================================================ FILE: electron/mapi/file/main.ts ================================================ import { dialog, ipcMain } from "electron"; import fileIndex from "./index"; ipcMain.handle( "file:openFile", async ( event, options: { filters?: { name: string; extensions: string[]; }[]; properties?: ("multiSelections" | "openFile")[]; } = {}, ): Promise => { options = Object.assign( { filters: [], properties: [], }, options, ); if (!options.properties.includes("openFile")) { options.properties.push("openFile"); } // @ts-ignore options.properties.push("noResolveAliases"); const res = await dialog .showOpenDialog({ ...options, }) .catch((e) => {}); if (!res || res.canceled) { return null; } if (options.properties.includes("multiSelections")) { return res.filePaths || null; } return res.filePaths?.[0] || null; }, ); ipcMain.handle( "file:openDirectory", async (_, options): Promise => { const res = await dialog .showOpenDialog({ properties: ["openDirectory"], ...options, }) .catch((e) => {}); if (!res || res.canceled) { return null; } return res.filePaths?.[0] || null; }, ); ipcMain.handle("file:openSave", async (_, options): Promise => { const res = await dialog .showSaveDialog({ ...options, }) .catch((e) => {}); if (!res || res.canceled) { return null; } return res.filePath || null; }); const autoCleanTemp = async () => { fileIndex.autoCleanTemp(1).finally(() => { setTimeout( () => { autoCleanTemp(); }, 10 * 60 * 1000, ); }); }; setTimeout(() => { autoCleanTemp().then(); }, 5000); export default { ...fileIndex, }; export const Files = { ...fileIndex, }; ================================================ FILE: electron/mapi/file/render.ts ================================================ import fileIndex from "./index"; import { ipcRenderer } from "electron"; const openFile = async (options: {} = {}) => { return ipcRenderer.invoke("file:openFile", options); }; const openDirectory = async (options: {} = {}) => { return ipcRenderer.invoke("file:openDirectory", options); }; const openSave = async (options: {} = {}) => { return ipcRenderer.invoke("file:openSave", options); }; export default { ...fileIndex, openFile, openDirectory, openSave, }; ================================================ FILE: electron/mapi/httpserver/main.ts ================================================ import type { Request, Response } from "express"; import express from "express"; import crypto from "node:crypto"; import fs from "node:fs"; import http from "node:http"; import path from "node:path"; import { AppEnv } from "../env"; import { Log } from "../log/main"; import { Manager } from "../manager/manager"; let server: http.Server | null = null; let isRunning = false; let runningPort = 0; let runningToken = ""; const getAvailablePort = (): Promise => { return new Promise((resolve, reject) => { const s = http.createServer(); s.listen(0, "127.0.0.1", () => { const addr = s.address() as { port: number }; const port = addr.port; s.close(() => resolve(port)); }); s.on("error", reject); }); }; const generateToken = (): string => { return ( crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "") ); }; const writeCliAuthFile = (port: number, token: string): void => { try { const filePath = path.join(AppEnv.userData, "cli-auth.json"); fs.writeFileSync(filePath, JSON.stringify({ port, token }), "utf-8"); } catch (e) { Log.error("httpserver.writeCliAuthFile.error", e); } }; const sendJson = (res: Response, statusCode: number, data: any) => { res.status(statusCode).json(data); }; const createApp = (port: number, token: string) => { const app = express(); app.use(express.json()); app.use((_req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); res.setHeader( "Access-Control-Allow-Headers", "Content-Type, Authorization", ); if (_req.method === "OPTIONS") { res.status(200).end(); return; } next(); }); app.use((_req, res, next) => { const auth = _req.headers["authorization"] || ""; if (!auth.startsWith("Bearer ") || auth.slice(7) !== token) { sendJson(res, 401, { code: -1, msg: "Unauthorized" }); return; } next(); }); app.get("/api/plugin/list", async (_req: Request, res: Response) => { try { const plugins = await Manager.listPlugin(); const list = plugins.map((p) => ({ name: p.name, title: p.title, version: p.version, logo: p.logo, type: p.type, description: p.description || "", })); sendJson(res, 200, { code: 0, data: { list } }); } catch (e) { sendJson(res, 500, { code: -1, msg: String(e) }); } }); return app; }; export const HttpServer = { async start() { if (isRunning) { return; } try { const port = await getAvailablePort(); const token = generateToken(); const app = createApp(port, token); server = http.createServer(app); await new Promise((resolve, reject) => { server!.listen(port, "127.0.0.1", () => resolve()); server!.on("error", reject); }); runningPort = port; runningToken = token; isRunning = true; writeCliAuthFile(port, token); Log.info("httpserver.start", { port }); } catch (e) { Log.error("httpserver.start.error", e); } }, stop() { if (server) { server.close(); server = null; isRunning = false; runningPort = 0; runningToken = ""; } }, getPort() { return runningPort; }, getToken() { return runningToken; }, }; ================================================ FILE: electron/mapi/keys/main.ts ================================================ import { app, BrowserWindow, globalShortcut } from "electron"; import { AppsMain } from "../app/main"; import { ManagerHotkey } from "../manager/hotkey"; const eventListeners = {}; // 连续点击的快捷键 let continuousKeys = []; const addKeyInput = (key: string, expire = 1000) => { let now = Date.now(); continuousKeys.push({ key, expire: now + expire }); continuousKeys = continuousKeys.filter((item) => item.expire > now); for (let i = continuousKeys.length - 1; i >= 0; i--) { const key = continuousKeys .filter((o, oIndex) => oIndex >= i) .map((o) => o.key) .join("|"); if (eventListeners[key]) { eventListeners[key](); break; } } }; const addMultiKeyListener = (keys: string[], callback: Function) => { if (!Array.isArray(keys)) { keys = [keys]; } const key = keys.join("|"); eventListeners[key] = callback; }; const createKeyInputListener = (key: string) => { return () => { addKeyInput(key); }; }; const keyMap = { "CommandOrControl+Shift+H": createKeyInputListener( "CommandOrControl+Shift+H", ), }; const ready = () => { register(); }; const destroy = () => { globalShortcut.unregisterAll(); }; const register = () => { globalShortcut.unregisterAll(); app.on("browser-window-focus", () => { for (let key in keyMap) { globalShortcut.register(key, keyMap[key]); } }); app.on("browser-window-blur", () => { for (let key in keyMap) { globalShortcut.unregister(key); } }); addMultiKeyListener( [ "CommandOrControl+Shift+H", "CommandOrControl+Shift+H", "CommandOrControl+Shift+H", ], () => { let focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) { if (focusedWindow.webContents.isDevToolsOpened()) { focusedWindow.webContents.closeDevTools(); } else { focusedWindow.webContents.openDevTools({ mode: "detach", activate: false, title: "FocusedWindow", }); } } }, ); ManagerHotkey.register().then(); }; export const KeysMain = { register, }; export default { ready, destroy, }; ================================================ FILE: electron/mapi/keys/type.ts ================================================ export enum HotkeyMouseButtonEnum { LEFT = 1, RIGHT = 2, } export type HotkeyKeyItem = { key: string; // Alt Option altKey: boolean; // Ctrl Control ctrlKey: boolean; // Command Win metaKey: boolean; // Shift shiftKey: boolean; times: number; }; export type HotkeyKeySimpleItem = { type: "Ctrl" | "Alt" | "Meta"; times: number; }; export type HotkeyMouseItem = { button: HotkeyMouseButtonEnum; type: "click" | "longPress"; clickTimes?: number; }; ================================================ FILE: electron/mapi/kvdb/kvdb.ts ================================================ import path from "path"; import fs from "fs"; import PouchDB from "pouchdb"; import { DBError, Doc, DocRes } from "./types"; import replicationStream from "pouchdb-replication-stream"; import load from "pouchdb-load"; import { KVDBVersionManager } from "./version"; import { Log } from "../log/main"; import ndj from "ndjson"; import through from "through2"; import { WebDav } from "./webdav"; import { AppEnv } from "../env"; PouchDB.plugin(replicationStream.plugin); // @ts-ignore PouchDB.adapter("writableStream", replicationStream.adapters.writableStream); PouchDB.plugin({ loadIt: load.load }); export default class KVDB { readonly docMaxByteLength; readonly docAttachmentMaxByteLength; public dbpath; public defaultDbName; public pouchDB: any; public versionControl: boolean; constructor() { // 2M this.docMaxByteLength = 2 * 1024 * 1024; // 20M this.docAttachmentMaxByteLength = 20 * 1024 * 1024; let dbPath = AppEnv.dataRoot; if (fs.existsSync(path.join(AppEnv.userData, "kvdb"))) { dbPath = AppEnv.userData; } this.dbpath = dbPath; this.defaultDbName = path.join(dbPath, "kvdb"); this.versionControl = true; } init(): void { fs.existsSync(this.dbpath) || fs.mkdirSync(this.dbpath, { recursive: true, }); this.pouchDB = new PouchDB(this.defaultDbName, { auto_compaction: true, adapter: "leveldb", }); } getDocId(name: string, id: string): string { return name + "/" + id; } replaceDocId(name: string, id: string): string { return id.replace(name + "/", ""); } errorInfo(name: string, message: string): DBError { return { error: true, name, message }; } private checkDocSize(doc: Doc) { if (Buffer.byteLength(JSON.stringify(doc)) > this.docMaxByteLength) { return this.errorInfo( "exception", `doc max size ${this.docMaxByteLength / 1024 / 1024} M`, ); } return false; } async put( name: string, doc: Doc, strict = true, ): Promise { if (strict) { const err = this.checkDocSize(doc); if (err) return err; } doc._id = this.getDocId(name, doc._id); try { const result: DocRes = await this.pouchDB.put(doc); if (this.versionControl) { if (doc._rev) { KVDBVersionManager.update(doc._id).then(); } else { KVDBVersionManager.insert(doc._id).then(); } } doc._id = result.id = this.replaceDocId(name, result.id); return result; } catch (e: any) { doc._id = this.replaceDocId(name, doc._id); return { id: doc._id, name: e.name, error: !0, message: e.message }; } } async putRaw(doc: Doc): Promise { let result: Doc | null = null; try { result = await this.pouchDB.get(doc._id); } catch (e) {} if (result) { doc._rev = result._rev; } try { return await this.pouchDB.put(doc); } catch (e: any) { return { id: doc._id, name: e.name, error: !0, message: e.message }; } } async get(name: string, id: string): Promise { try { const result: Doc = await this.pouchDB.get(this.getDocId(name, id)); result._id = this.replaceDocId(name, result._id); return result; } catch (e) { return null; } } async getRaw(id: string) { try { return await this.pouchDB.get(id); } catch (e) { return null; } } async remove(name: string, doc: Doc | string) { try { let target; if ("object" == typeof doc) { target = doc; if (!target._id || "string" !== typeof target._id) { return this.errorInfo("exception", "doc _id error"); } target._id = this.getDocId(name, target._id); } else { if ("string" !== typeof doc) { return this.errorInfo("exception", "param error"); } target = await this.pouchDB.get(this.getDocId(name, doc)); } const result: DocRes = await this.pouchDB.remove(target); if (this.versionControl) { KVDBVersionManager.remove(target._id).then(); } target._id = result.id = this.replaceDocId(name, result.id); return result; } catch (e: any) { if ("object" === typeof doc) { doc._id = this.replaceDocId(name, doc._id); } return this.errorInfo(e.name, e.message); } } async removeRaw(doc: Doc) { try { return await this.pouchDB.remove(doc); } catch (e) { return null; } } async bulkPut( name: string, docs: Array>, ): Promise> { let result; try { if (!Array.isArray(docs)) return this.errorInfo("exception", "not array"); if (docs.find((e) => !e._id)) return this.errorInfo("exception", "doc not _id field"); if (new Set(docs.map((e) => e._id)).size !== docs.length) return this.errorInfo("exception", "_id value exists as"); for (const doc of docs) { const err = this.checkDocSize(doc); if (err) return err; doc._id = this.getDocId(name, doc._id); } result = await this.pouchDB.bulkDocs(docs); result = result.map((res: any) => { res.id = this.replaceDocId(name, res.id); return res.error ? { id: res.id, name: res.name, error: true, message: res.message, } : res; }); docs.forEach((doc) => { if (this.versionControl) { if (doc._rev) { KVDBVersionManager.update(doc._id).then(); } else { KVDBVersionManager.insert(doc._id).then(); } } doc._id = this.replaceDocId(name, doc._id); }); } catch (e) { // } return result; } async all( name: string, key: string | Array, ): Promise>> { const config: any = { include_docs: true }; if (key) { if ("string" == typeof key) { config.startkey = this.getDocId(name, key); config.endkey = config.startkey + "￰"; } else { if (!Array.isArray(key)) return this.errorInfo( "exception", "param only key(string) or keys(Array[string])", ); config.keys = key.map((key) => this.getDocId(name, key)); } } else { config.startkey = this.getDocId(name, ""); config.endkey = config.startkey + "￰"; } const result: Array = []; try { (await this.pouchDB.allDocs(config)).rows.forEach((res: any) => { if (!res.error && res.doc) { res.doc._id = this.replaceDocId(name, res.doc._id); result.push(res.doc); } }); } catch (e) { // } return result; } async allKeys( name: string, key: string | Array, ): Promise> { const config: any = { include_docs: false }; if (key) { if ("string" == typeof key) { config.startkey = this.getDocId(name, key); config.endkey = config.startkey + "￰"; } else { if (!Array.isArray(key)) return this.errorInfo( "exception", "param only key(string) or keys(Array[string])", ); config.keys = key.map((key) => this.getDocId(name, key)); } } else { config.startkey = this.getDocId(name, ""); config.endkey = config.startkey + "￰"; } const result: Array = []; try { (await this.pouchDB.allDocs(config)).rows.forEach((res: any) => { if (!res.error && res.id) { const id = this.replaceDocId(name, res.id); result.push(id); } }); } catch (e) { // } return result; } async count( name: string, key: string | Array, ): Promise { const config: any = { include_docs: false }; if (key) { if ("string" == typeof key) { config.startkey = this.getDocId(name, key); config.endkey = config.startkey + "￰"; } else { if (!Array.isArray(key)) return this.errorInfo( "exception", "param only key(string) or keys(Array[string])", ); config.keys = key.map((key) => this.getDocId(name, key)); } } else { config.startkey = this.getDocId(name, ""); config.endkey = config.startkey + "￰"; } try { return (await this.pouchDB.allDocs(config)).rows.length; } catch (e) { // } return 0; } public async postAttachment( name: string, docId: string, attachment: Buffer | Uint8Array, type: string, ) { const buffer = Buffer.from(attachment); if (buffer.byteLength > this.docAttachmentMaxByteLength) return this.errorInfo( "exception", "attachment data up to " + this.docAttachmentMaxByteLength / 1024 / 1024 + "M", ); try { const result = await this.pouchDB.put({ _id: this.getDocId(name, docId), _attachments: { 0: { data: buffer, content_type: type } }, }); if (this.versionControl) { KVDBVersionManager.insert(result.id).then(); } result.id = this.replaceDocId(name, result.id); return result; } catch (e) { return this.errorInfo(e.name, e.message); } } async getAttachment(name: string, docId: string, len = "0") { try { return await this.pouchDB.getAttachment( this.getDocId(name, docId), len, ); } catch (e) { return null; } } async getAttachmentRaw(docId: string, len = "0") { try { return await this.pouchDB.getAttachment(docId, len); } catch (e) { return null; } } public async dumpToFile(file: string, option?: {}): Promise { try { const writeStream = fs.createWriteStream(file); await this.pouchDB.dump(writeStream, { batch_size: 10, }); } catch (e) { Log.info("kvdb.dumpToFile.error", e); throw e; } } public async importFromFile(file: string, option?: {}): Promise { await this.pouchDB.destroy(); const syncDb = new KVDB(); syncDb.init(); this.pouchDB = syncDb.pouchDB; const rs = fs.createReadStream(file); try { await this.load(rs); } catch (e) { Log.info("kvdb.importFromFile.error", e); throw e; } } public async dumpToWavDav( file: string, option: { url: string; username: string; password: string; }, ): Promise { try { const webdav = new WebDav(option); await webdav.dump(this, file); } catch (e) { Log.info("kvdb.dumpToWavDav.error", e); throw e; } } public async importFromWebDav( file: string, option: { url: string; username: string; password: string; }, ): Promise { await this.pouchDB.destroy(); const syncDb = new KVDB(); syncDb.init(); this.pouchDB = syncDb.pouchDB; try { const webdav = new WebDav(option); await webdav.import(this, file); } catch (e) { Log.info("kvdb.importFromWebDav.error", e); throw e; } } public async load(readableStream: any) { return new Promise((resolve, reject) => { let error = null; let queue = []; readableStream .pipe(ndj.parse()) .on("error", function (errorCatched) { error = errorCatched; }) .pipe( through.obj(function (data, _, next) { if (!data.docs) { return next(); } // lets smooth it out data.docs.forEach(function (doc) { this.push(doc); }, this); next(); }), ) .pipe( through.obj( function (doc, _, next) { // console.log('doc', doc) if (doc._attachments) { for (const k in doc._attachments) { if (doc._attachments[k].data) { // console.log('doc._attachments[k].data', k, doc._attachments[k].data) const bytes = doc._attachments[k].data.data; const base64 = new Buffer( bytes, ).toString("base64"); doc._attachments[k].data = base64; } } } queue.push(doc); if (queue.length >= 10) { this.push(queue); queue = []; } next(); }, function (next) { if (queue.length) { this.push(queue); } next(); }, ), ) .pipe(this.pouchDB.createWriteStream({ new_edits: false })) .on("error", function (errorCatched) { error = errorCatched; }) .on("finish", function () { if (error) { reject(error); } else { resolve(undefined); } }); }); } } ================================================ FILE: electron/mapi/kvdb/main.ts ================================================ import KVDB from "./kvdb"; import { AppEnv } from "../env"; import { DBError, Doc } from "./types"; import { ipcMain } from "electron"; import { WebDav } from "./webdav"; let kvdb: KVDB = null; const init = () => { kvdb = new KVDB(); kvdb.init(); // for (let i = 0; i < 1000; i++) { // kvdb.putRaw({ // _id: `data${i}`, // data: i // }) // } // const sync = async () => { // KVDBCloudManager.sync().then(() => { // setTimeout(sync, 5000) // }) // } // setTimeout(sync, 1000) }; const raw = () => { return kvdb; }; const put = async (name: string, data: Doc) => { const result = await kvdb.put(name, data); if (result && (result as DBError).error) { throw (result as DBError).message; } return result as Doc; }; const putForceLock = new Map>(); const putForce = async (name: string, data: Doc) => { while (putForceLock.has(name)) { await putForceLock.get(name); } let release!: () => void; const currentTask = new Promise((resolve) => { release = resolve; }); putForceLock.set(name, currentTask); try { const res = await get(name, data._id); if (res) { data._rev = res._rev; } const result = await put(name, data); if (result && (result as DBError).error) { throw (result as DBError).message; } return result as Doc; } finally { putForceLock.delete(name); release(); } }; const get = async (name: string, id: string) => { return await kvdb.get(name, id); }; const getData = async (name: string, id: string, defaultValue: any = null) => { const res = await get(name, id); if (res) { delete res._id; delete res._rev; delete res._attachments; } return res ? res : defaultValue; }; const remove = async (name: string, doc: Doc | string) => { return await kvdb.remove(name, doc); }; const bulkDocs = async (name: string, docs: any[]) => { const result = await kvdb.bulkPut(name, docs); if (result && (result as DBError).error) { throw (result as DBError).message; } return result as Doc[]; }; const allDocs = async (name: string, key: string): Promise => { const result = await kvdb.all(name, key); if (result && (result as DBError).error) { throw (result as DBError).message; } return result as Doc[]; }; const allKeys = async (name: string, key: string): Promise => { const result = await kvdb.allKeys(name, key); if (result && (result as DBError).error) { throw (result as DBError).message; } return result as string[]; }; const count = async (name: string, key: string) => { const result = await kvdb.count(name, key); if (result && (result as DBError).error) { throw (result as DBError).message; } return result as number; }; const postAttachment = async ( name: string, docId: string, attachment: any, type: string, ) => { return await kvdb.postAttachment(name, docId, attachment, type); }; const getAttachment = async (name: string, docId: string) => { return await kvdb.getAttachment(name, docId); }; const getAttachmentType = async (name: string, docId: string) => { const res = await get(name, docId); if (!res || !res._attachments) return null; const result = res._attachments[0]; return result ? result.content_type : null; }; const dumpToFile = async (file: string) => { return await kvdb.dumpToFile(file); }; const importFromFile = async (file: string) => { return await kvdb.importFromFile(file); }; const testWebdav = async (option: { url: string; username: string; password: string; }) => { const webdav = new WebDav(option); await webdav.checkConnection(); }; const dumpToWebDav = async ( file: string, option: { url: string; username: string; password: string; }, ) => { return await kvdb.dumpToWavDav(file, option); }; const importFromWebDav = async ( file: string, option: { url: string; username: string; password: string; }, ) => { return await kvdb.importFromWebDav(file, option); }; const listWebDav = async ( dir: string, option: { url: string; username: string; password: string; }, ) => { const webdav = new WebDav(option); await webdav.checkConnection(); return await webdav.listDir(dir); }; ipcMain.handle("kvdb:put", (event, name: string, data: Doc) => { return put(name, data); }); ipcMain.handle("kvdb:putForce", (event, name: string, data: Doc) => { return putForce(name, data); }); ipcMain.handle("kvdb:get", (event, name: string, id: string) => { return get(name, id); }); ipcMain.handle("kvdb:remove", (event, name: string, doc: Doc | string) => { return remove(name, doc); }); ipcMain.handle("kvdb:bulkDocs", (event, name: string, docs: any[]) => { return bulkDocs(name, docs); }); ipcMain.handle("kvdb:allDocs", (event, name: string, key: string) => { return allDocs(name, key); }); ipcMain.handle("kvdb:allKeys", (event, name: string, key: string) => { return allKeys(name, key); }); ipcMain.handle("kvdb:count", (event, name: string, key: string) => { return count(name, key); }); ipcMain.handle( "kvdb:postAttachment", (event, name: string, docId: string, attachment: any, type: string) => { return postAttachment(name, docId, attachment, type); }, ); ipcMain.handle("kvdb:getAttachment", (event, name: string, docId: string) => { return getAttachment(name, docId); }); ipcMain.handle( "kvdb:getAttachmentType", (event, name: string, docId: string) => { return getAttachmentType(name, docId); }, ); ipcMain.handle("kvdb:dumpToFile", (event, file: string) => { return dumpToFile(file); }); ipcMain.handle("kvdb:importFromFile", (event, file: string) => { return importFromFile(file); }); ipcMain.handle( "kvdb:testWebdav", ( event, option: { url: string; username: string; password: string; }, ) => { return testWebdav(option); }, ); ipcMain.handle( "kvdb:dumpToWebDav", ( event, file: string, option: { url: string; username: string; password: string; }, ) => { return dumpToWebDav(file, option); }, ); ipcMain.handle( "kvdb:importFromWebDav", ( event, file: string, option: { url: string; username: string; password: string; }, ) => { return importFromWebDav(file, option); }, ); ipcMain.handle( "kvdb:listWebDav", ( event, dir: string, option: { url: string; username: string; password: string; }, ) => { return listWebDav(dir, option); }, ); export const KVDBMain = { raw, put, putForce, get, getData, remove, bulkDocs, allDocs, allKeys, postAttachment, getAttachment, getAttachmentType, dumpToFile, importFromFile, dumpToWebDav, importFromWebDav, listWebDav, }; export default { init, ...KVDBMain, }; ================================================ FILE: electron/mapi/kvdb/render.ts ================================================ import { Doc } from "./types"; import { ipcRenderer } from "electron"; const put = async (name: string, doc: Doc) => { return ipcRenderer.invoke("kvdb:put", name, doc); }; const putForce = async (name: string, doc: Doc) => { return ipcRenderer.invoke("kvdb:putForce", name, doc); }; const get = async (name: string, id: string) => { return ipcRenderer.invoke("kvdb:get", name, id); }; const remove = async (name: string, doc: Doc | string) => { return ipcRenderer.invoke("kvdb:remove", name, doc); }; const bulkDocs = async (name: string, docs: any[]) => { return ipcRenderer.invoke("kvdb:bulkDocs", name, docs); }; const allDocs = async (name: string, key: string) => { return ipcRenderer.invoke("kvdb:allDocs", name, key); }; const allKeys = async (name: string, key: string) => { return ipcRenderer.invoke("kvdb:allKeys", name, key); }; const count = async (name: string, key: string) => { return ipcRenderer.invoke("kvdb:count", name, key); }; const postAttachment = async ( name: string, docId: string, attachment: any, type: string, ) => { return ipcRenderer.invoke( "kvdb:postAttachment", name, docId, attachment, type, ); }; const getAttachment = async (name: string, docId: string) => { return ipcRenderer.invoke("kvdb:getAttachment", name, docId); }; const getAttachmentType = async (name: string, docId: string) => { return ipcRenderer.invoke("kvdb:getAttachmentType", name, docId); }; const dumpToFile = async (file: string) => { return ipcRenderer.invoke("kvdb:dumpToFile", file); }; const importFromFile = async (file: string) => { return ipcRenderer.invoke("kvdb:importFromFile", file); }; const testWebdav = async (option: { url: string; username: string; password: string; }) => { return ipcRenderer.invoke("kvdb:testWebdav", option); }; const dumpToWebDav = async ( file: string, option: { url: string; username: string; password: string; }, ) => { return ipcRenderer.invoke("kvdb:dumpToWebDav", file, option); }; const importFromWebDav = async ( file: string, option: { url: string; username: string; password: string; }, ) => { return ipcRenderer.invoke("kvdb:importFromWebDav", file, option); }; const listWebDav = async ( dir: string, option: { url: string; username: string; password: string; }, ) => { return ipcRenderer.invoke("kvdb:listWebDav", dir, option); }; export default { put, putForce, get, remove, bulkDocs, allDocs, allKeys, count, postAttachment, getAttachment, getAttachmentType, dumpToFile, importFromFile, testWebdav, dumpToWebDav, importFromWebDav, listWebDav, }; ================================================ FILE: electron/mapi/kvdb/types.ts ================================================ type RevisionId = string; export type Doc> = { _id: string; _rev?: string; _attachments?: any; } & T; export interface DocRes { id: string; ok: boolean; rev: RevisionId; _id: string; data?: any; } export interface DBError { status?: number | undefined; name?: string | undefined; message?: string | undefined; reason?: string | undefined; error?: string | boolean | undefined; id?: string | undefined; rev?: RevisionId | undefined; } export interface AllDocsOptions { include_docs?: boolean; startkey?: string; endkey?: string; keys?: string[]; } ================================================ FILE: electron/mapi/kvdb/version.ts ================================================ import DBMain from "../db/main"; import { StrUtil } from "../../lib/util"; export const KVDBVersionManager = { async _getExist(name: string) { const records = await DBMain.select( "select * from kvdb_data where name = ? and isDeleted = 0", [name], ); for (let i = 1; i < records.length; i++) { await DBMain.delete("delete from kvdb_data where id = ?", [ records[i].id, ]); } return records.length > 0 ? records[0] : null; }, async update(name: string) { if (this.shouldIgnore(name)) { return; } // console.log('update', {name}) const exist = await this._getExist(name); if (exist) { await DBMain.update( "update kvdb_data set version = -1 where id = ?", [exist.id], ); } else { await DBMain.insert( "insert into kvdb_data (id, name, version, cloudVersion, isDeleted) values (?,?,-1,0,0)", [StrUtil.bigIntegerId(), name], ); } }, async insert(name: string) { await this.update(name); }, async remove(name: string) { if (this.shouldIgnore(name)) { return; } const exist = await this._getExist(name); // console.log('remove', {name, exist}) if (exist) { await DBMain.update( "update kvdb_data set isDeleted = 1, version = -1 where id = ?", [exist.id], ); } else { await DBMain.insert( "insert into kvdb_data (id, name, version, cloudVersion, isDeleted ) values (?, ?, -1, 0, 1)", [StrUtil.bigIntegerId(), name], ); } }, shouldIgnore(name: string) { return [ // 系统存储版本号的kvdb "SYS/syncCloudVersion", ].includes(name); }, }; ================================================ FILE: electron/mapi/kvdb/webdav.ts ================================================ import MemoryStream from "memorystream"; import { AuthType, createClient } from "webdav"; import { WebDAVClient } from "webdav/dist/node/types"; import KVDB from "./kvdb"; type WebDavOptions = { username: string; password: string; url: string; }; export class WebDav { public client: WebDAVClient; constructor({ username, password, url }: WebDavOptions) { // console.log('WebDavOptions', {username, password, url}) this.client = createClient(url, { authType: AuthType.Auto, username, password, }); } async checkConnection(): Promise { await this.client.exists("/"); } async listDir(dir: string): Promise { dir = dir.endsWith("/") ? dir : dir + "/"; const result = await this.client.getDirectoryContents(dir); return (result as any[]).map((item) => item.basename); } async dump(kvdb: KVDB, file: string): Promise { await this.checkConnection(); const fileDir = file.substring(0, file.lastIndexOf("/")); if (!(await this.client.exists(fileDir + "/"))) { await this.client.createDirectory(fileDir, { recursive: true, }); } const ws = new MemoryStream(); kvdb.pouchDB.dump(ws, { batch_size: 10, }); return new Promise((resolve, reject) => { ws.pipe( this.client.createWriteStream(file, {}, () => { resolve(); }), ); }); } async import(kvdb: KVDB, file: string): Promise { // console.log('import', file) await this.checkConnection(); if (!(await this.client.exists(file))) { throw "FileNotFound"; } const rs = this.client.createReadStream(file); await kvdb.load(rs); } } ================================================ FILE: electron/mapi/log/beacon-render.ts ================================================ /** * 渲染进程异常上报(HTTP Beacon) * 仅在 isPackaged(非开发)模式下上报,批量异步发送。 */ import { AppConfig } from "../../../src/config"; declare const __BUILD_ID__: string; const BEACON_URL = "https://g.tecmz.com/grow/load.gif"; const BEACON_APP = "focusany"; const isPackaged = process.env["IS_PACKAGED"] === "true"; const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const buildId = typeof __BUILD_ID__ !== "undefined" ? __BUILD_ID__ : "unknown"; interface BeaconEvent { et: "error"; path: string; did: string; sid: string; ts: number; type: string; bid: string; props: { msg: string; stack?: string; src?: string; line?: number; col?: number; }; } let pending: BeaconEvent[] = []; let timer: ReturnType | null = null; const flush = () => { if (!pending.length) return; const events = pending.splice(0); try { const encoded = encodeURIComponent(btoa(JSON.stringify(events))); const url = `${BEACON_URL}?app=${BEACON_APP}&data=${encoded}`; fetch(url).catch(() => {}); } catch {} }; const schedule = () => { if (timer) return; timer = setTimeout(() => { timer = null; flush(); }, 3000); }; export const reportErrorRender = ( msg: string, stack?: string, src?: string, line?: number, col?: number, path = "/renderer", ) => { if (!isPackaged) return; pending.push({ et: "error", path, did: "renderer", sid: sessionId, ts: Date.now(), type: `app-${AppConfig.version}`, bid: buildId, props: { msg, stack, src, line, col }, }); schedule(); }; ================================================ FILE: electron/mapi/log/beacon.ts ================================================ /** * 主进程异常上报(HTTP Beacon) * 仅在 isPackaged(非开发)模式下上报,批量异步发送。 */ import https from "node:https"; import { AppConfig } from "../../../src/config"; import { isPackaged, platformUUID } from "../../lib/env"; declare const __BUILD_ID__: string; const BEACON_URL = "https://g.tecmz.com/grow/load.gif"; const BEACON_APP = "focusany"; const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const buildId = typeof __BUILD_ID__ !== "undefined" ? __BUILD_ID__ : "unknown"; interface BeaconEvent { et: "error"; path: string; did: string; sid: string; ts: number; type: string; bid: string; props: { msg: string; stack?: string }; } let pending: BeaconEvent[] = []; let timer: ReturnType | null = null; let _did: string | null = null; const getDid = (): string => { if (_did) return _did; try { _did = platformUUID() || "unknown"; } catch { _did = "unknown"; } return _did; }; const flush = () => { if (!pending.length) return; const events = pending.splice(0); try { const encoded = encodeURIComponent( Buffer.from(JSON.stringify(events)).toString("base64"), ); const url = `${BEACON_URL}?app=${BEACON_APP}&data=${encoded}`; https.get(url).on("error", () => {}); } catch {} }; const schedule = () => { if (timer) return; timer = setTimeout(() => { timer = null; flush(); }, 3000); }; export const reportError = (msg: string, stack?: string, path = "/main") => { if (!isPackaged) return; pending.push({ et: "error", path, did: getDid(), sid: sessionId, ts: Date.now(), type: `app-${AppConfig.version}`, bid: buildId, props: { msg, stack }, }); schedule(); }; ================================================ FILE: electron/mapi/log/index.ts ================================================ import electron from "electron"; import date from "date-and-time"; import path from "node:path"; import { AppEnv } from "../env"; import fs from "node:fs"; import dayjs from "dayjs"; import FileIndex from "../file"; let fileName = null; let fileStream = null; let appFileNames = {}; let appFileStreams = {}; const stringDatetime = () => { return date.format(new Date(), "YYYYMMDD"); }; const jsonStringifyLogData = (data: any) => { return JSON.stringify(data, (key, value) => { if (typeof value === "string" && value.length > 200) { if ( value.startsWith("data:") || value.substring(0, 190).match(/^[a-zA-Z0-9+/=]+\s*$/) ) { return ( value.substring(0, 100) + "...(length=" + value.length + ")" ); } } return value; }); }; const logsDir = () => { return path.join(AppEnv.userData, "logs"); }; const appLogsDir = () => { return path.join(AppEnv.dataRoot, "logs"); }; const root = () => { return logsDir(); }; const file = () => { return path.join(logsDir(), "log_" + stringDatetime() + ".log"); }; const appFile = (name: string) => { return path.join(appLogsDir(), name + "_" + stringDatetime() + ".log"); }; const cleanOldLogs = (keepDays: number) => { const logDirs = [ // 系统日志 logsDir(), // 应用日志 appLogsDir(), ]; for (const logDir of logDirs) { if (!fs.existsSync(logDir)) { return; } const files = fs.readdirSync(logDir); const now = new Date(); // console.log('cleanOldLogs', logDir, files) for (let file of files) { const filePath = path.join(logDir, file); let date = null; for (let s of file.split(/[_\\.]/)) { // 匹配 YYYYMMDD if (s.match(/^\d{8}$/)) { date = s; break; } } if (!date) { continue; } const fileDate = new Date( parseInt(date.substring(0, 4)), parseInt(date.substring(4, 6)) - 1, parseInt(date.substring(6, 8)), ); const diff = Math.abs(now.getTime() - fileDate.getTime()); const diffDays = Math.ceil(diff / (1000 * 3600 * 24)); // console.log('fileDate', file, fileDate, diffDays) if (diffDays > keepDays) { fs.unlinkSync(filePath); } } } }; const log = (level: "INFO" | "ERROR", label: string, data: any = null) => { if (fileName !== file()) { fileName = file(); const logDir = logsDir(); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir); } if (fileStream) { fileStream.end(); } fileStream = fs.createWriteStream(fileName, { flags: "a" }); cleanOldLogs(14); } let line = []; line.push(date.format(new Date(), "YYYY-MM-DD HH:mm:ss")); line.push(level); line.push(label); if (data) { if (!["number", "string"].includes(typeof data)) { data = jsonStringifyLogData(data); } line.push(data); } console.log(line.join(" - ")); fileStream.write(line.join(" - ") + "\n"); }; const info = (label: string, data: any = null) => { return log("INFO", label, data); }; const error = (label: string, data: any = null) => { return log("ERROR", label, data); }; const appLog = ( name: string, level: "INFO" | "ERROR", label: string, data: any = null, ) => { let fileChanged = false; if (appFileNames[name] !== appFile(name)) { appFileNames[name] = appFile(name); fileChanged = true; } if (fileChanged || !appFileStreams[name]) { if (appFileStreams[name]) { appFileStreams[name].end(); } const logDir = appLogsDir(); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir); } appFileStreams[name] = fs.createWriteStream(appFileNames[name], { flags: "a", }); } let line = []; line.push(date.format(new Date(), "YYYY-MM-DD HH:mm:ss")); line.push(level); line.push(label); if (data) { if (!["number", "string"].includes(typeof data)) { data = JSON.stringify(data); } line.push(data); } console.log(`[APP:${name}] - ` + line.join(" - ")); appFileStreams[name].write(line.join(" - ") + "\n"); }; const appPath = (name: string) => { if (!appFileNames[name]) { appFileNames[name] = appFile(name); } return appFileNames[name]; }; const appInfo = (name: string, label: string, data: any = null) => { return appLog(name, "INFO", label, data); }; const appError = (name: string, label: string, data: any = null) => { return appLog(name, "ERROR", label, data); }; const infoRenderOrMain = (label: string, data: any = null) => { if (electron.ipcRenderer) { console.log("Log.info", label, data); return electron.ipcRenderer.invoke("log:info", label, data); } else { return info(label, data); } }; const errorRenderOrMain = (label: string, data: any = null) => { if (electron.ipcRenderer) { console.error("Log.error", label, data); return electron.ipcRenderer.invoke("log:error", label, data); } else { return error(label, data); } }; const appInfoRenderOrMain = (name: string, label: string, data: any = null) => { if (electron.ipcRenderer) { console.log("Log.appInfo", name, label, data); return electron.ipcRenderer.invoke("log:appInfo", name, label, data); } else { return appInfo(name, label, data); } }; const appErrorRenderOrMain = ( name: string, label: string, data: any = null, ) => { if (electron.ipcRenderer) { console.error("Log.appError", name, label, data); return electron.ipcRenderer.invoke("log:appError", name, label, data); } else { return appError(name, label, data); } }; const collectRenderOrMain = async (option?: { startTime?: string; endTime?: string; limit?: number; }) => { option = Object.assign( { startTime: dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm:ss"), endTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), limit: 10 * 10000, }, option, ); let startMs = dayjs(option.startTime).valueOf(); let endMs = dayjs(option.endTime).valueOf(); let startDayMs = dayjs(option.startTime).startOf("day").valueOf(); let endDayMs = dayjs(option.endTime).endOf("day").valueOf(); let resultLines = []; let logFiles = []; logFiles = logFiles.concat( await FileIndex.list(logsDir(), { isDataPath: false }), ); logFiles = logFiles.concat( await FileIndex.list(appLogsDir(), { isDataPath: false }), ); // console.log('logFiles', logFiles) logFiles = logFiles.filter((logFile) => { if (logFile.isDirectory) { return false; } let date = null; for (let s of logFile.name.split(/[_\\.]/)) { // 匹配 YYYYMMDD if (s.match(/^\d{8}$/)) { date = s; break; } } if (!date) { return false; } const fileDate = new Date( parseInt(date.substring(0, 4)), parseInt(date.substring(4, 6)) - 1, parseInt(date.substring(6, 8)), ); if (fileDate.getTime() < startDayMs || fileDate.getTime() > endDayMs) { return false; } return true; }); // console.log('collectRenderOrMain', { // ...option, // logFiles, startMs, endMs, startDayMs, endDayMs // }) for (const logFile of logFiles) { await FileIndex.readLine( logFile.pathname, (line) => { const lineParts = line.split(" - "); const lineTime = dayjs(lineParts[0]); // console.log('lineTime', lineParts[0], lineTime.isBefore(startMs) || lineTime.isAfter(endMs)) if (lineTime.isBefore(startMs) || lineTime.isAfter(endMs)) { return; } resultLines.push(line); }, { isDataPath: false }, ); } return { startTime: option.startTime, endTime: option.endTime, logs: resultLines.join("\n"), }; }; export default { root, info, error, infoRenderOrMain, errorRenderOrMain, appPath, appInfo, appError, appInfoRenderOrMain, appErrorRenderOrMain, collectRenderOrMain, jsonStringifyLogData, }; export const Log = { jsonStringifyLogData, info: infoRenderOrMain, error: errorRenderOrMain, appPath, appInfo: appInfoRenderOrMain, appError: appErrorRenderOrMain, }; ================================================ FILE: electron/mapi/log/main.ts ================================================ import { ipcMain } from "electron"; import logIndex from "./index"; ipcMain.handle("log:info", (event, label: string, data: any) => { logIndex.info(label, data); }); ipcMain.handle("log:error", (event, label: string, data: any) => { logIndex.error(label, data); }); ipcMain.handle( "log:appInfo", (event, name: string, label: string, data: any) => { logIndex.appInfo(name, label, data); }, ); ipcMain.handle( "log:appError", (event, name: string, label: string, data: any) => { logIndex.appError(name, label, data); }, ); export default { info: logIndex.info, error: logIndex.error, appInfo: logIndex.appInfo, appError: logIndex.appError, }; export const Log = { info: logIndex.info, error: logIndex.error, appPath: logIndex.appPath, appInfo: logIndex.appInfo, appError: logIndex.appError, jsonStringifyLogData: logIndex.jsonStringifyLogData, }; ================================================ FILE: electron/mapi/log/render.ts ================================================ import logIndex from "./index"; export default { root: logIndex.root, info: logIndex.infoRenderOrMain, error: logIndex.errorRenderOrMain, appInfo: logIndex.appInfoRenderOrMain, appError: logIndex.appErrorRenderOrMain, collect: logIndex.collectRenderOrMain, }; ================================================ FILE: electron/mapi/main.ts ================================================ import app from "./app/main"; import config from "./config/main"; import db from "./db/main"; import event from "./event/main"; import file from "./file/main"; import { HttpServer } from "./httpserver/main"; import keys from "./keys/main"; import kvdb from "./kvdb/main"; import log from "./log/main"; import manager from "./manager/main"; import misc from "./misc/main"; import protocol from "./protocol/main"; import storage from "./storage/main"; import ui from "./ui"; import updater from "./updater/main"; import user from "./user/main"; const $mapi = { app, log, config, storage, db, file, event, ui, keys, user, misc, protocol, updater, manager, kvdb, }; export const MAPI = { async init() { await $mapi.user.init(); await $mapi.db.init(); await $mapi.event.init(); $mapi.kvdb.init(); $mapi.manager.init(); HttpServer.start().then(); }, ready() { $mapi.keys.ready(); $mapi.manager.ready(); $mapi.protocol.ready(); }, destroy() { $mapi.keys.destroy(); $mapi.manager.destroy(); }, }; ================================================ FILE: electron/mapi/manager/automation/index.ts ================================================ import { Button, Key, keyboard, mouse } from "@nut-tree-fork/nut-js"; import { activeWindow, Result } from "get-windows"; import { windowManager } from "node-window-manager"; import { Window } from "node-window-manager/src/classes/window"; import { ActiveWindow, ClipboardDataType } from "../../../../src/types/Manager"; import { Log } from "../../log/main"; import { ManagerClipboard } from "../clipboard"; export const ManagerAutomation = { init() { ManagerAutomation.track(); }, lastWindow: null as Result | null, lastWindowManager: null as Window | null, track() { windowManager.on("window-activated", async (win) => { if (win) { if ( !ManagerAutomation.lastWindow || win.id !== ManagerAutomation.lastWindow.id ) { if (!ManagerAutomation.trackShouldIgnore(win)) { ManagerAutomation.lastWindow = win; ManagerAutomation.lastWindowManager = windowManager.getActiveWindow(); } } } }); }, trackShouldIgnore(win: Window): boolean { if (!win || !win.id) { return true; } if (["Electron", "FocusAny"].includes(win.getTitle())) { return true; } // if (['FocusAny'].includes(win.getOwner()?.name)) { // return true; // } Log.info("ManagerAutomation.track", { win, title: win.getTitle(), owner: win.getOwner(), }); return false; }, async activateLatestWindow(): Promise { if (ManagerAutomation.lastWindowManager) { ManagerAutomation.lastWindowManager.bringToTop(); } }, async getActiveWindow(): Promise { const win = { name: "", title: "", attr: {}, raw: null, } as ActiveWindow; const active = await activeWindow(); if (active) { win.raw = active; win.name = active.owner?.name || ""; win.title = active.title; if ("url" in active) { win.attr["url"] = active.url + ""; } } return win; }, async typeString(text: string): Promise { // await keyboard.type(text); await ManagerClipboard.pasteClipboardContent({ type: "text", text: text, } as ClipboardDataType); }, async typeKey(key: string): Promise { const keyMap: { [key: string]: Key } = { a: Key.A, b: Key.B, c: Key.C, d: Key.D, e: Key.E, f: Key.F, g: Key.G, h: Key.H, i: Key.I, j: Key.J, k: Key.K, l: Key.L, m: Key.M, n: Key.N, o: Key.O, p: Key.P, q: Key.Q, r: Key.R, s: Key.S, t: Key.T, u: Key.U, v: Key.V, w: Key.W, x: Key.X, y: Key.Y, z: Key.Z, "0": Key.Num0, "1": Key.Num1, "2": Key.Num2, "3": Key.Num3, "4": Key.Num4, "5": Key.Num5, "6": Key.Num6, "7": Key.Num7, "8": Key.Num8, "9": Key.Num9, space: Key.Space, enter: Key.Enter, tab: Key.Tab, backspace: Key.Backspace, delete: Key.Delete, escape: Key.Escape, shift: Key.LeftShift, control: Key.LeftControl, alt: Key.LeftAlt, command: Key.LeftSuper, left: Key.Left, right: Key.Right, up: Key.Up, down: Key.Down, f1: Key.F1, f2: Key.F2, f3: Key.F3, f4: Key.F4, f5: Key.F5, f6: Key.F6, f7: Key.F7, f8: Key.F8, f9: Key.F9, f10: Key.F10, f11: Key.F11, f12: Key.F12, }; const nutKey = keyMap[key.toLowerCase()]; if (nutKey) { await keyboard.pressKey(nutKey); } }, async mouseToggle( type: "down" | "up", button: "left" | "right" | "middle", ): Promise { const buttonMap: { [key: string]: Button } = { left: Button.LEFT, right: Button.RIGHT, middle: Button.MIDDLE, }; const nutButton = buttonMap[button]; if (nutButton) { if (type === "down") { await mouse.pressButton(nutButton); } else { await mouse.releaseButton(nutButton); } } }, async moveMouse(x: number, y: number): Promise { await mouse.setPosition({ x, y }); }, async mouseClick( button: "left" | "right" | "middle", double: boolean = false, ): Promise { const buttonMap: { [key: string]: Button } = { left: Button.LEFT, right: Button.RIGHT, middle: Button.MIDDLE, }; const nutButton = buttonMap[button]; if (nutButton) { if (double) { await mouse.doubleClick(nutButton); } else { await mouse.click(nutButton); } } }, }; ================================================ FILE: electron/mapi/manager/backend/index.ts ================================================ import { ActionRecord, PluginRecord } from "../../../../src/types/Manager"; import { ManagerSystem } from "../system"; import fs from "node:fs"; import { ImportUtil } from "../../../lib/util"; import { PluginSdkCreate } from "../plugin/sdk"; import { PluginLog } from "../plugin/log"; export const ManagerBackend = { async run( plugin: PluginRecord, type: "hook" | "event" | "action" | "mcpTool", key: string, data: any, option?: { rejectIfError: boolean; }, ) { option = Object.assign( { rejectIfError: false, }, option, ); try { if (!plugin.runtime?.root) { throw `PluginRootNotFound:${plugin.name}:${type}:${key}`; } const backendPath = `${plugin.runtime?.root}/backend.cjs`; if (!fs.existsSync(backendPath)) { if (option.rejectIfError) { throw `BackendFileNotFound:${backendPath}`; } return; } const backend = await ImportUtil.loadCommonJs(backendPath); if (!(type in backend)) { if (option.rejectIfError) { throw `BackendTypeNotFound:${type}`; } return; } if (!(key in backend[type])) { if (option.rejectIfError) { throw `BackendKeyNotFound:${type}.${key}`; } return; } const func = backend[type][key]; const sdk = PluginSdkCreate(plugin); return await new Promise((resolve, reject) => { Promise.resolve(func(sdk, data)).then(resolve).catch(reject); }); } catch (e) { PluginLog.error(plugin.name, `Backend.Run.Error-${type}-${key}`, { error: e + "", data, option, }); } }, async runAction(plugin: PluginRecord, action: ActionRecord, option?: {}) { const codeData = {}; codeData["actionName"] = action.name; codeData["actionMatch"] = action.runtime?.match; try { const callback = ManagerSystem.getActionBackendFunc( plugin.name, action.name, ); if (callback) { return await callback(codeData); } return await this.run(plugin, "action", action.name, codeData, { rejectIfError: true, }); } catch (e) { PluginLog.error( plugin.name, `Backend.RunAction.Error:${action.name}`, e + "", ); } }, }; ================================================ FILE: electron/mapi/manager/clipboard/clipboardFiles.ts ================================================ import { clipboard } from "electron"; import plist from "plist"; import fs from "fs"; import path from "path"; import ofs from "original-fs"; import { isLinux, isMac, isWin } from "../../../lib/env"; let electronClipboardEx = null; if (isMac || isWin) { (async () => { try { electronClipboardEx = await import("electron-clipboard-ex"); electronClipboardEx = electronClipboardEx.default; } catch (e) {} })(); } export const getClipboardFiles = (): FileItem[] => { let fileInfo: any; if (isMac) { if (!clipboard.has("NSFilenamesPboardType")) { return []; } const result = clipboard.read("NSFilenamesPboardType"); if (!result) { return []; } try { fileInfo = plist.parse(result); } catch (e) { return []; } } else if (isWin) { try { /* eslint-disable */ fileInfo = electronClipboardEx.readFilePaths(); } catch (e) { // todo } } else if (isLinux) { if (!clipboard.has("text/uri-list")) { return []; } const result = clipboard .read("text/uri-list") .match(/^file:\/\/\/.*/gm); if (!result || !result.length) { return []; } fileInfo = result.map((e) => decodeURIComponent(e).replace(/^file:\/\//, ""), ); } if (!Array.isArray(fileInfo)) { return []; } const target: any = fileInfo .map((p) => { if (!fs.existsSync(p)) return false; let info; try { info = ofs.lstatSync(p); } catch (e) { return false; } let fileExt = null; if (info.isFile()) { fileExt = path.extname(p).toLowerCase().replace(/^./, ""); } return { isFile: info.isFile(), isDirectory: info.isDirectory(), name: path.basename(p) || p, path: p, fileExt: fileExt, }; }) .filter(Boolean); return target.length ? target : []; }; export const setClipboardFiles = (files: string[]) => { if (!files || !files.length) { return; } if (isMac) { clipboard.writeBuffer( "NSFilenamesPboardType", Buffer.from(plist.build(files)), ); } else if (isWin) { electronClipboardEx.writeFilePaths(files); } else if (isLinux) { // @ts-ignore clipboard.write( "text/uri-list", files.map((e) => `file://${e}`).join("\n"), ); } }; ================================================ FILE: electron/mapi/manager/clipboard/index.ts ================================================ import { AppsMain } from "../../app/main"; import { ClipboardDataType, ClipboardHistoryRecord, } from "../../../../src/types/Manager"; import { Files } from "../../file/main"; import { EncodeUtil, FileUtil, sleep, StrUtil, TimeUtil, } from "../../../lib/util"; import StorageMain from "../../storage/main"; import { getClipboardFiles, setClipboardFiles } from "./clipboardFiles"; import { clipboard } from "electron"; import { isMac } from "../../../lib/env"; import { KeyboardKey, ManagerHotkeySimulate } from "../hotkey/simulate"; import { ManagerPluginEvent } from "../plugin/event"; import { Log } from "../../log/main"; export const ManagerClipboard = { MAX_LIMIT: 1000, running: true, interval: 1000, timer: null, watchNextTime: 0, lastContentJson: null, lastChangeTimestamp: 0, encryptKey: null, clipboardBusy: false, clipboardBackupData: null as ClipboardDataType | null, async init() { this.encryptKey = await StorageMain.get( "clipboard", "encryptKey", null, ); if (!this.encryptKey) { this.encryptKey = StrUtil.randomString(16); await StorageMain.set("clipboard", "encryptKey", this.encryptKey); } this.monitorStart(); // console.log('all', await this.list()) }, async waitClipboardFree() { while (this.clipboardBusy) { await sleep(10); } }, async backupClipboard() { await this.waitClipboardFree(); this.clipboardBusy = true; this.clipboardBackupData = await this._getClipboardContent(); clipboard.clear(); }, async restoreClipboard() { clipboard.clear(); if (this.clipboardBackupData) { await this._setClipboardContent(this.clipboardBackupData); this.clipboardBackupData = null; } this.clipboardBusy = false; }, async getSelectedContent(): Promise { await this.backupClipboard(); ManagerHotkeySimulate.keyTap(KeyboardKey.C, [ isMac ? KeyboardKey.Meta : KeyboardKey.Ctrl, ]); await new Promise((resolve) => setTimeout(resolve, 200)); const select = await this._getClipboardContent(); await this.restoreClipboard(); return select; }, async _setClipboardContent(data: ClipboardDataType): Promise { switch (data.type) { case "file": setClipboardFiles(data.files.map((file) => file.path)); break; case "image": AppsMain.setClipboardImage(data.image); break; case "text": AppsMain.setClipboardText(data.text); break; } }, async _getClipboardContent(): Promise { const files = getClipboardFiles(); if (files.length) { return { type: "file", files: files, } as ClipboardDataType; } const image = AppsMain.getClipboardImage(); if (image) { return { type: "image", image: image, } as ClipboardDataType; } const text = AppsMain.getClipboardText(); if (text) { return { type: "text", text: text, } as ClipboardDataType; } return null; }, async pasteClipboardContent(data: ClipboardDataType): Promise { if (!data) { return; } await this.backupClipboard(); await this._setClipboardContent(data); ManagerHotkeySimulate.keyTap(KeyboardKey.V, [ isMac ? KeyboardKey.Meta : KeyboardKey.Ctrl, ]); await sleep(200); await this.restoreClipboard(); }, async getClipboardContent(): Promise { await this.waitClipboardFree(); const content = await this._getClipboardContent(); this.watchNextTime = Date.now() + this.interval; const contentJson = JSON.stringify(content); if ( null == this.lastContentJson || contentJson !== this.lastContentJson ) { if (this.lastContentJson) { this.lastChangeTimestamp = TimeUtil.timestamp(); } this.lastContentJson = contentJson; this.onChange(content).then(); } return content; }, _watch() { if (this.watchNextTime > Date.now()) { setTimeout( () => { this._watch(); }, Math.max(this.watchNextTime - Date.now(), 0), ); return; } this.getClipboardContent().finally(() => { if (this.running) { setTimeout( () => { this._watch(); }, Math.max(this.watchNextTime - Date.now(), 0), ); } }); }, monitorStart() { this.running = true; this._watch(); }, monitorStop() { this.running = false; }, encrypt(data: ClipboardHistoryRecord) { const dataJson = JSON.stringify(data); return EncodeUtil.aesEncode(dataJson, this.encryptKey); }, decrypt(data: string): ClipboardHistoryRecord { try { data = EncodeUtil.aesDecode(data, this.encryptKey); return JSON.parse(data) as ClipboardHistoryRecord; } catch (e) { return null; } }, async onChange(data: ClipboardDataType) { if (!data) { return; } // console.log('clipboard.onChange', data) const filename = TimeUtil.timestampDayStart(); const saveData = { type: data.type, timestamp: TimeUtil.timestamp(), files: data.files, image: data.image, text: data.text, } as ClipboardHistoryRecord; if (saveData.image) { const imageMd5 = EncodeUtil.md5(saveData.image); let imageFile = `clipboard/${filename}/${imageMd5}`; Files.writeBuffer( imageFile, FileUtil.base64ToBuffer(saveData.image), { isDataPath: true }, ).then(); saveData.image = imageMd5; } const dataString = this.encrypt(saveData); // console.log('clipboard.write', `clipboard/${filename}/data`, dataString) await Files.appendText( `clipboard/${filename}/data`, `${dataString}\n`, { isDataPath: true }, ); await ManagerPluginEvent.firePluginEvent("ClipboardChange", saveData); }, async list(limit: number = -1): Promise { const fullPath = await Files.fullPath("clipboard"); const dateDir = await Files.list("clipboard", { isDataPath: true }); // 按照倒序排列 pathname dateDir.sort((a, b) => { return b.pathname.localeCompare(a.pathname); }); const result = []; let maxLimitReached = false; for (const dir of dateDir) { if (maxLimitReached) { await Files.deletes(`clipboard/${dir.name}`, { isDataPath: true, }); continue; } const data = await Files.read(`clipboard/${dir.name}/data`, { isDataPath: true, }); if (!data) { await Files.deletes(`clipboard/${dir.name}`, { isDataPath: true, }); Log.error( "ManagerClipboard.list", `Deleted empty clipboard directory: clipboard/${dir.name}`, ); continue; } for (const line of data.split("\n").reverse()) { if (!line) { continue; } const record = this.decrypt(line); if (!record) { continue; } if (record.image) { record.image = `file://${fullPath}/${dir.name}/${record.image}`; } result.push(record); if (limit > 0 && result.length >= limit) { break; } if (result.length > ManagerClipboard.MAX_LIMIT) { maxLimitReached = true; break; } } if (limit > 0 && result.length >= limit) { break; } } return result; }, async clear() { await Files.deletes("clipboard", { isDataPath: true }); }, async delete(timestamp: number) { const date = TimeUtil.timestampDayStart(timestamp * 1000); const data = await Files.read(`clipboard/${date}/data`, { isDataPath: true, }); const lines = data.split("\n"); const result = []; for (const line of lines) { if (!line) { continue; } const record = this.decrypt(line); if (!record) { continue; } if (record.timestamp !== timestamp) { result.push(line); } } await Files.write(`clipboard/${date}/data`, result.join("\n"), { isDataPath: true, }); }, }; ================================================ FILE: electron/mapi/manager/code/index.ts ================================================ import { ActionRecord, PluginRecord } from "../../../../src/types/Manager"; import { ManagerSystem } from "../system"; import { ManagerWindow } from "../window"; import { PluginLog } from "../plugin/log"; export const ManagerCode = { async execute(plugin: PluginRecord, action: ActionRecord, option?: {}) { try { const codeData = {}; codeData["actionName"] = action.name; codeData["actionMatch"] = action.runtime?.match; codeData["requestId"] = action.runtime?.requestId; const callback = ManagerSystem.getActionCodeFunc( plugin.name, action.name, ); if (callback) { return await callback(codeData); } return await ManagerWindow.openForCode(plugin, action, { codeData, }); } catch (e) { PluginLog.error(plugin.name, `Code.Execute.Error`, { error: e + "", action, option, }); } }, }; ================================================ FILE: electron/mapi/manager/config/config.ts ================================================ import { ActionRecord, ConfigRecord, LaunchRecord, PluginActionRecord, PluginConfig, PluginRecord, } from "../../../../src/types/Manager"; import { KVDBMain } from "../../kvdb/main"; import { CommonConfig } from "../../../config/common"; import { ManagerHotkey } from "../hotkey"; import { MemoryCacheUtil } from "../../../lib/util"; import { ManagerPlugin } from "../plugin"; import { ManagerSystem } from "../system"; import { isLinux, isMac, isWin } from "../../../lib/env"; const defaultConfig: ConfigRecord = { mainTrigger: { key: "Space", altKey: true, ctrlKey: false, metaKey: false, shiftKey: false, times: 1, }, detachWindowTrigger: { key: "D", altKey: false, ctrlKey: !isMac, metaKey: isMac, shiftKey: false, times: 1, }, fastPanelTrigger: { type: "Ctrl", times: 1, }, // fastPanelTriggerButton: { // button: HotkeyMouseButtonEnum.RIGHT, // type: 'longPress', // }, }; export const ManagerConfig = { configOld: null as ConfigRecord | null, async clearCache() { MemoryCacheUtil.forget("Config"); MemoryCacheUtil.forget("DisabledActionMatches"); MemoryCacheUtil.forget("PinActions"); MemoryCacheUtil.forget("Launches"); MemoryCacheUtil.forget("CustomActions"); MemoryCacheUtil.forget("HistoryActions"); MemoryCacheUtil.forget("PluginConfig"); }, async get(): Promise { return MemoryCacheUtil.remember("Config", async () => { // reset config // await this.save(defaultConfig) const config = await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbConfigId, ); if (!config) { await this.save(defaultConfig); this.configOld = defaultConfig; return defaultConfig; } let changed = false; for (const key in defaultConfig) { if (key in config) { if (typeof config[key] === "object") { for (const subKey in defaultConfig[key]) { if (subKey in config[key]) { } else { config[key][subKey] = defaultConfig[key][subKey]; changed = true; } } } } else { config[key] = defaultConfig[key]; changed = true; } } if (changed) { await this.save(config); } this.configOld = config; return config; }); }, async save(config: ConfigRecord): Promise { // delete config["data"]; const doc = { _id: CommonConfig.dbConfigId, ...config, }; await KVDBMain.putForce(CommonConfig.dbSystem, doc); let hotkeyChanged = false; if (this.configOld) { for (const k of ["mainTrigger", "fastPanelTrigger"]) { if ( JSON.stringify(this.configOld[k]) !== JSON.stringify(config[k]) ) { hotkeyChanged = true; break; } } } MemoryCacheUtil.forget("Config"); if (hotkeyChanged) { ManagerHotkey.configInit().then(); } }, async listDisabledActionMatch() { return MemoryCacheUtil.remember("DisabledActionMatches", async () => { return ( (await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbDisabledActionMatchId, )) || {} ); }); }, async toggleDisabledActionMatch( pluginName: string, actionName: string, matchName: string, ) { let matches = await this.listDisabledActionMatch(); if (!matches) { matches = {}; } if (!matches[pluginName]) { matches[pluginName] = {}; } if (!matches[pluginName][actionName]) { matches[pluginName][actionName] = []; } let disabled = false; if (matches[pluginName][actionName].includes(matchName)) { matches[pluginName][actionName] = matches[pluginName][ actionName ].filter((v) => v !== matchName); if (!matches[pluginName][actionName].length) { delete matches[pluginName][actionName]; } if (!Object.keys(matches[pluginName]).length) { delete matches[pluginName]; } } else { matches[pluginName][actionName].push(matchName); disabled = true; } await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbDisabledActionMatchId, ...matches, }); MemoryCacheUtil.forget("DisabledActionMatches"); return disabled; }, async listPinAction(): Promise { return MemoryCacheUtil.remember("PinActions", async () => { const res = await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbPinActionId, ); if (!res) { return []; } return res["records"] || []; }); }, async getPinedActionSet(): Promise> { const pinActions = await this.listPinAction(); const set = new Set(); for (const pinAction of pinActions) { set.add(`${pinAction.pluginName}/${pinAction.actionName}`); } return set; }, async togglePinAction(pluginName: string, actionName: string) { let pinActions = await this.listPinAction(); const saveAction = { pluginName: pluginName, actionName: actionName, } as PluginActionRecord; const exists = pinActions.find( (v) => v.pluginName === saveAction.pluginName && v.actionName === saveAction.actionName, ); if (exists) { pinActions = pinActions.filter( (v) => v.pluginName !== saveAction.pluginName || v.actionName !== saveAction.actionName, ); } else { pinActions.unshift(saveAction); } await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbPinActionId, records: pinActions, }); MemoryCacheUtil.forget("PinActions"); }, async listLaunch(): Promise { return MemoryCacheUtil.remember("Launches", async () => { const res = await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbLaunchId, ); if (!res) { return []; } return res["records"] || []; }); }, async updateLaunch(records: LaunchRecord[]) { // normalize records records.forEach((record) => { if (!("type" in record)) { // @ts-ignore record.type = "custom"; } if (!("name" in record)) { // @ts-ignore record.name = ""; } }); // sort records by type(custom,plugin) and name(a-z) records.sort((a, b) => { if (a.type === b.type) { return a.name.localeCompare(b.name); } return a.type.localeCompare(b.type); }); await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbLaunchId, records: records, }); MemoryCacheUtil.forget("Launches"); ManagerHotkey.configInit().then(); }, async getCustomAction(): Promise> { return MemoryCacheUtil.remember("CustomActions", async () => { return ( (await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbCustomActionId, )) || {} ); }); }, async addCustomAction( plugin: PluginRecord, action: ActionRecord | ActionRecord[], ) { const customAction = await this.getCustomAction(); if (!(plugin.name in customAction)) { customAction[plugin.name] = []; } if (!Array.isArray(action)) { action = [action]; } for (let a of action) { a = ManagerPlugin.normalAction(a, plugin); let replace = false; for (let i = 0; i < customAction[plugin.name].length; i++) { if (customAction[plugin.name][i].name === a.name) { customAction[plugin.name][i] = a; replace = true; break; } } if (!replace) { customAction[plugin.name].push(a); } } await this.updateCustomAction(customAction); }, async updateCustomAction(customAction: Record) { await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbCustomActionId, ...customAction, }); MemoryCacheUtil.forget("CustomActions"); }, async removeCustomAction(plugin: PluginRecord, name: string) { const customAction = await this.getCustomAction(); if (!(plugin.name in customAction)) { return; } customAction[plugin.name] = customAction[plugin.name].filter( (v) => v.name !== name, ); await this.updateCustomAction(customAction); }, async clearCustomAction(pluginName: string) { const customAction = await this.getCustomAction(); if (!(pluginName in customAction)) { return; } delete customAction[pluginName]; await this.updateCustomAction(customAction); }, async getHistoryAction(): Promise { return MemoryCacheUtil.remember("HistoryActions", async () => { const res = await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbHistoryActionId, ); if (!res) { return []; } return res["records"] || []; }); }, async clearHistoryAction() { await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbHistoryActionId, records: [], }); MemoryCacheUtil.forget("HistoryActions"); }, async deleteHistoryAction(pluginName: string, actionName: string) { // console.log('deleteHistoryAction', fullName) let historyActions = await this.getHistoryAction(); historyActions = historyActions.filter( (v) => v.pluginName !== pluginName || v.actionName !== actionName, ); await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbHistoryActionId, records: historyActions, }); MemoryCacheUtil.forget("HistoryActions"); }, async addHistoryAction(plugin: PluginRecord, action: ActionRecord) { let historyActions = await this.getHistoryAction(); const saveAction = { pluginName: plugin.name, actionName: action.name, } as PluginActionRecord; // remove duplicate historyActions = historyActions.filter( (v) => v.pluginName !== saveAction.pluginName || v.actionName !== saveAction.actionName, ); historyActions.unshift(saveAction); if (historyActions.length > 100) { historyActions.pop(); } await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbHistoryActionId, records: historyActions, }); MemoryCacheUtil.forget("HistoryActions"); }, async getPluginConfigAll(): Promise> { return MemoryCacheUtil.remember("PluginConfig", async () => { const res = await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbPluginConfigId, ); if (!res) { return {}; } return res["records"] || {}; }); }, async getPluginConfig(pluginName: string): Promise { const res = await this.getPluginConfigAll(); return res[pluginName] || {}; }, async setPluginConfigItem(pluginName: string, key: string, value: any) { const config = await this.getPluginConfig(pluginName); config[key] = value; await ManagerConfig.setPluginConfig(pluginName, config); if (ManagerSystem.match(pluginName)) { await ManagerSystem.clearCache(); } else { await ManagerPlugin.clearCache(); } }, async setPluginConfig(pluginName: string, config: PluginConfig) { const pluginConfig = await this.getPluginConfigAll(); pluginConfig[pluginName] = config; await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbPluginConfigId, records: pluginConfig, }); MemoryCacheUtil.forget("PluginConfig"); }, }; ================================================ FILE: electron/mapi/manager/editor/index.ts ================================================ import nodePath from "node:path"; import { t } from "../../../config/lang"; import { AppsMain } from "../../app/main"; import { Files } from "../../file/main"; import { Log } from "../../log/main"; import { Manager } from "../manager"; import { SearchQuery } from "../type"; export const ManagerEditor = { filePath: null, isReady: false, async init() {}, async ready() { this.isReady = true; }, faDataTypeCache: {}, async getFaDataTypeCached(file: string) { if (file in this.faDataTypeCache) { return this.faDataTypeCache[file]; } const fileExt = nodePath.extname(file).toLowerCase(); if (fileExt !== ".fad") { return null; } try { const result = await Files.read(file, { isDataPath: false, }); const json = JSON.parse(result); this.faDataTypeCache[file] = json["type"]; } catch (e) { this.faDataTypeCache[file] = null; } return this.faDataTypeCache[file]; }, async filterFadType(files: FileItem[], types: string[]) { const newFiles = []; for (const file of files) { const fileExt = nodePath.extname(file.path).toLowerCase(); if (fileExt !== ".fad") { continue; } const fileType = await this.getFaDataTypeCached(file.path); if (!fileType) { continue; } if (types.includes(fileType)) { newFiles.push(file); } } return newFiles; }, async openQueue(filePath: string) { this.filePath = filePath; await this.openFileEditor(); }, async openFileEditor() { return new Promise(async (resolve) => { const run = async () => { if (!this.isReady) { setTimeout(run, 100); return; } if (!this.filePath) { Log.info( "ManagerEditor.openFileEditor.Empty", this.filePath, ); return; } if ( !(await Files.exists(this.filePath, { isDataPath: false })) ) { Log.info( "ManagerEditor.openFileEditor.NotFound", this.filePath, ); return; } const fileExt = nodePath.extname(this.filePath).toLowerCase(); const file: FileItem = { name: nodePath.basename(this.filePath), isDirectory: false, isFile: true, path: this.filePath, fileExt: fileExt.replace(".", ""), }; const actions = await Manager.matchActionSimple({ currentFiles: [file], activeWindow: null, } as SearchQuery); // Log.info('ManagerEditor.openFileEditor.Actions', JSON.stringify(actions, null, 2)) if (actions.length > 0) { Manager.openAction( JSON.parse(JSON.stringify(actions[0])), ).then(); } else { AppsMain.toast(t("editor.noPluginForFile"), { status: "error", }); } resolve(undefined); }; run().then(); }); }, }; ================================================ FILE: electron/mapi/manager/hotkey/handle.ts ================================================ import { ManagerPluginEvent } from "../plugin/event"; import { ManagerConfig } from "../config/config"; import ConfigMain from "../../config/main"; export const ManagerHotkeyHandle = { async mainTrigger() { if (await ManagerPluginEvent.isMainWindowShown(null, null)) { if (await ManagerPluginEvent.isMainWindowFocused(null, null)) { await ManagerPluginEvent.hideMainWindow(null, null); } else { await ManagerPluginEvent.showMainWindow(null, null); } } else { await ManagerPluginEvent.showMainWindow(null, null); } }, async fastPanelTrigger() { if (!(await ConfigMain.get("fastPanelEnable", true))) { return; } if (await ManagerPluginEvent.isFastPanelWindowShown(null, null)) { await ManagerPluginEvent.hideFastPanelWindow(null, null); } else { await ManagerPluginEvent.showFastPanelWindow(null, null); } }, async launch(index: string) { const i = parseInt(index); const launches = await ManagerConfig.listLaunch(); if (i < launches.length) { await ManagerPluginEvent.redirect(null, { keywordsOrAction: launches[i].keyword, }); } }, }; ================================================ FILE: electron/mapi/manager/hotkey/index.ts ================================================ import { uIOhook, UiohookKey } from "uiohook-napi"; import { ManagerConfig } from "../config/config"; import { ManagerHotkeyHandle } from "./handle"; import { HotkeyMouseButtonEnum } from "../../keys/type"; import { Events } from "../../event/main"; import { KeysMain } from "../../keys/main"; import { globalShortcut } from "electron"; type HotkeyKeyItem = { name: string; keycode: any; altKey: boolean; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; times: number; expireTimer?: number; }; type HotkeyKeySimpleItem = { name: string; type: "Ctrl" | "Alt" | "Meta"; times: number; }; type HotkeyMouseItem = { name: string; button: HotkeyMouseButtonEnum; type: "click" | "longPress"; clickTimes?: number; expireTime?: number; expireCount?: number; }; const keyToKeyCode = (key: string) => { if (key in UiohookKey) { return UiohookKey[key]; } return 0; }; const keyCodeToKey = (keyCode: number) => { for (const key in UiohookKey) { if (UiohookKey[key] === keyCode) { return key; } } return ""; }; export const ManagerHotkey = { isGrab: false, keyMultiDelayTime: 500, keyConfigs: [ // { // name: 'mainTrigger', // keycode: UiohookKey.Space, // altKey: false, // ctrlKey: false, // metaKey: true, // shiftKey: false, // times: 1, // }, ] as HotkeyKeyItem[], keySimpleConfigs: [ // { // name: 'fastPanelTrigger', // type: 'Ctrl', // times: 2, // } ] as HotkeyKeySimpleItem[], mouseLongPressTime: 500, mouseConfigs: [ // { // name: 'fastPanelTrigger', // type: 'click', // button: HotkeyButtonEnum.RIGHT, // clickTimes: 1, // }, // { // name: 'fastPanelTrigger2', // type: 'longPress', // button: HotkeyButtonEnum.RIGHT, // } ] as HotkeyMouseItem[], _keySimple: { // Ctrl: null as null | 'down' | 'up', // Alt: null as null | 'down' | 'up', // Meta: null as null | 'down' | 'up', down: null as null | "Ctrl" | "Alt" | "Meta", key: null as null | "Ctrl" | "Alt" | "Meta", expire: 0, times: 0, }, init() { uIOhook.on("keydown", (e) => { if (this.isGrab) { const data = { type: "keydown", key: keyCodeToKey(e.keycode), altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey, }; Events.broadcast("HotkeyWatch", data); return; } // console.log('ManagerHotkey.keydown', e, this.keyConfigs) // keyConfigs start for (const item of this.keyConfigs) { if ( item.keycode !== e.keycode || item.altKey !== e.altKey || item.ctrlKey !== e.ctrlKey || item.metaKey !== e.metaKey || item.shiftKey !== e.shiftKey ) { continue; } if (!item.times || item.times <= 1) { this.fire(item.name); return; } const now = Date.now(); if (!item.expireTime || now > item.expireTime) { item.expireTime = now + this.keyMultiDelayTime; item.expireCount = 1; } else { item.expireCount++; if (item.expireCount >= item.times) { this.fire(item.name); item.expireTime = 0; item.expireCount = 0; return; } } } // keyConfigs end // keySimpleConfigs start if ( e.keycode === UiohookKey.Ctrl && !e.altKey && e.ctrlKey && !e.metaKey && !e.shiftKey ) { this._keySimple.down = "Ctrl"; } else if ( e.keycode === UiohookKey.Alt && e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey ) { this._keySimple.down = "Alt"; } else if ( e.keycode === UiohookKey.Meta && !e.altKey && !e.ctrlKey && e.metaKey && !e.shiftKey ) { this._keySimple.down = "Meta"; } else { this._keySimple.down = null; } // keySimpleConfigs end }); const keySimpleUp = (key: "Ctrl" | "Alt" | "Meta") => { // console.log('keySimpleUp', key, JSON.stringify(this.keySimpleConfigs)) const now = Date.now(); if (this._keySimple.expire > now && key === this._keySimple.key) { this._keySimple.times++; } else { this._keySimple.times = 1; this._keySimple.key = key; } this._keySimple.expire = now + this.keyMultiDelayTime; this.keySimpleConfigs .filter( (o) => o.type === key && o.times <= this._keySimple.times, ) .forEach((o) => this.fire(o.name)); }; uIOhook.on("keyup", (e) => { if ( e.keycode === UiohookKey.Ctrl && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && this._keySimple.down === "Ctrl" ) { keySimpleUp("Ctrl"); } else if ( e.keycode === UiohookKey.Alt && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && this._keySimple.down === "Alt" ) { keySimpleUp("Alt"); } else if ( e.keycode === UiohookKey.Meta && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && this._keySimple.down === "Meta" ) { keySimpleUp("Meta"); } }); // uIOhook.on('mousedown', (e) => { // // console.log('ManagerHotkey.mousedown', e) // for (const item of this.mouseConfigs) { // if (item.button !== e.button) { // continue // } // if (item.type === 'click') { // if (!item.clickTimes || item.clickTimes <= 1) { // this.fire(item.name) // } else if (item.clickTimes === e.clicks) { // this.fire(item.name) // } // } else if (item.type === 'longPress') { // item.expireTimer = setTimeout(() => { // this.fire(item.name) // item.expireTimer = 0 // }, this.mouseLongPressTime) // } // } // }) // uIOhook.on('mouseup', (e) => { // // console.log('ManagerHotkey.mouseup', e) // for (const item of this.mouseConfigs) { // if (item.button === HotkeyMouseButtonEnum.LEFT && e.button !== 1) { // continue // } // if (item.button === HotkeyMouseButtonEnum.RIGHT && e.button !== 2) { // continue // } // if (item.type === 'longPress') { // if (item.expireTimer) { // clearTimeout(item.expireTimer) // item.expireTimer = 0 // } // } // } // }) uIOhook.start(); this.configInit().then(); }, destroy() { uIOhook.stop(); }, async register() { // console.log('ManagerHotkey.register', this.keyConfigs) for (const keyConfig of this.keyConfigs) { const accelerator = []; if (keyConfig.ctrlKey) { accelerator.push("Control"); } if (keyConfig.metaKey) { accelerator.push("Meta"); } if (keyConfig.altKey) { accelerator.push("Alt"); } if (keyConfig.shiftKey) { accelerator.push("Shift"); } accelerator.push(keyCodeToKey(keyConfig.keycode)); globalShortcut.register(accelerator.join("+"), () => { this.fire(keyConfig.name); }); } this.keyConfigs = this.keyConfigs.filter( (item) => item.times && item.times > 1, ); }, async configInit() { this.keyConfigs = []; const config = await ManagerConfig.get(); for (const k of ["mainTrigger"]) { if (config[k]) { this.keyConfigs.push({ name: k, keycode: keyToKeyCode(config[k].key), altKey: config[k].altKey, ctrlKey: config[k].ctrlKey, metaKey: config[k].metaKey, shiftKey: config[k].shiftKey, times: config[k].times, }); } } this.keySimpleConfigs = []; if (config.fastPanelTrigger) { this.keySimpleConfigs.push({ name: "fastPanelTrigger", type: config.fastPanelTrigger.type, times: config.fastPanelTrigger.times || 1, }); } const launches = await ManagerConfig.listLaunch(); launches.forEach((launch, launchIndex) => { if (launch.hotkey && launch.keyword) { this.keyConfigs.push({ name: `launch:${launchIndex}`, keycode: keyToKeyCode(launch.hotkey.key), altKey: launch.hotkey.altKey, ctrlKey: launch.hotkey.ctrlKey, metaKey: launch.hotkey.metaKey, shiftKey: launch.hotkey.shiftKey, times: launch.hotkey.times, }); } }); // this.mouseConfigs = [] // if (config.fastPanelTriggerButton) { // this.mouseConfigs.push({ // name: 'fastPanelTrigger', // type: config.fastPanelTriggerButton.type, // button: config.fastPanelTriggerButton.button, // clickTimes: config.fastPanelTriggerButton.clickTimes, // }) // } KeysMain.register(); }, async watch() { this.isGrab = true; }, async unwatch() { this.isGrab = false; }, eventListeners: {}, fire(eventName: string, ...args: any[]) { // console.log('ManagerHotkey.fire', eventName, args) let eventParam = ""; if (eventName.includes(":")) { const pcs = eventName.split(":"); if (pcs.length > 1) { eventName = pcs[0]; eventParam = pcs[1]; } } if (eventName in ManagerHotkeyHandle) { ManagerHotkeyHandle[eventName](eventParam); } if (!this.eventListeners[eventName]) { return; } this.eventListeners[eventName].forEach((cb) => cb(...args)); }, on(eventName: string, callback: Function) { if (!this.eventListeners[eventName]) { this.eventListeners[eventName] = []; } this.eventListeners[eventName].push(callback); }, off(eventName: string, callback: Function) { if (!this.eventListeners[eventName]) { return; } this.eventListeners[eventName] = this.eventListeners[eventName].filter( (cb) => cb !== callback, ); }, }; ================================================ FILE: electron/mapi/manager/hotkey/simulate.ts ================================================ import { uIOhook, UiohookKey } from "uiohook-napi"; export const KeyboardKey = { ...UiohookKey, }; export const ManagerHotkeySimulate = { toCode(key: string) { return key in KeyboardKey ? KeyboardKey[key] : key; }, keyTap(key: number, modifiers?: number[]) { uIOhook.keyTap(key, modifiers); }, }; ================================================ FILE: electron/mapi/manager/lib/cache.ts ================================================ import { Files } from "../../file/main"; import { Log } from "../../log/main"; export const ManagerFileCacheUtil = { async get(name: string, defaultValue: any = null) { const content = await Files.read(`cache/${name}.json`, { isDataPath: true, }); if (content) { let json = null; try { json = JSON.parse(content); } catch (e) { Log.error("Plugin.App.Error", e); } if (!json || !("expire" in json) || !("value" in json)) { await Files.deletes(`cache/${name}.json`, { isDataPath: true }); return defaultValue; } if (json.expire > 0 && json.expire < Date.now()) { await Files.deletes(`cache/${name}.json`, { isDataPath: true }); return defaultValue; } return json.value; } return defaultValue; }, async getIgnoreExpire( name: string, defaultValue: any = null, ): Promise<{ isCache: boolean; value: any; expire: number; }> { const content = await Files.read(`cache/${name}.json`, { isDataPath: true, }); if (content) { let json = null; try { json = JSON.parse(content); } catch (e) { Log.error("Plugin.App.Error", e); } if (!json || !("value" in json)) { await Files.deletes(`cache/${name}.json`, { isDataPath: true }); return { isCache: false, value: defaultValue, expire: 0, }; } return { isCache: true, value: json.value, expire: json.expire, }; } return { isCache: false, value: defaultValue, expire: 0, }; }, async set(name: string, value: any, expire: number = 0) { const json = { expire: expire > 0 ? Date.now() + expire : 0, value: value, }; await Files.write(`cache/${name}.json`, JSON.stringify(json), { isDataPath: true, }); }, async forget(name: string) { await Files.deletes(`cache/${name}.json`, { isDataPath: true }); }, }; ================================================ FILE: electron/mapi/manager/lib/hooks.ts ================================================ import { BrowserView, BrowserWindow } from "electron"; import { AppsMain } from "../../app/main"; import { PluginRecord } from "../../../../src/types/Manager"; type PluginHookType = | never | "PluginReady" | "PluginExit" | "PluginEvent" | "MoreMenuClick" | "DetachOperateClick" | "SubInputChange" | "ScreenCapture" | "Hotkey"; type HookType = | never | "Show" | "Hide" | "SetSubInput" | "RemoveSubInput" | "SetSubInputValue" | "PluginInit" | "PluginInitReady" | "PluginExit" | "PluginAlreadyOpened" | "PluginDetached" | "PluginState" | "PluginCodeInit" | "PluginCodeData" | "PluginCodeSetting" | "PluginCodeExit" | "DetachSet" | "Maximize" | "Unmaximize" | "EnterFullScreen" | "LeaveFullScreen" | "DetachWindowClosed"; export const executePluginHooks = async ( view: BrowserView, hook: PluginHookType, data?: any, ) => { const evalJs = ` if(window.focusany && window.focusany.hooks && typeof window.focusany.hooks.on${hook} === 'function' ) { try { window.focusany.hooks.on${hook}(${JSON.stringify(data)}); } catch(e) { console.log('executePluginHooks.on${hook}.error', e); } }`; return view.webContents?.executeJavaScript(evalJs); }; export const executeHooks = async ( win: BrowserWindow, hook: HookType, data?: any, ) => { const evalJs = ` if(window.__page && window.__page.hooks && typeof window.__page.hooks.on${hook} === 'function' ) { try { window.__page.hooks.on${hook}(${JSON.stringify(data)}); } catch(e) { console.log('executeHooks.on${hook}.error', e); } }`; return win.webContents?.executeJavaScript(evalJs); }; export const executeDarkMode = async ( view: BrowserWindow | BrowserView, data: { plugin: PluginRecord; isSystem: boolean; }, ) => { // console.log('executeDarkMode', data.plugin.setting); if ( (await AppsMain.shouldDarkMode()) && (data.plugin.setting?.darkModeSupport || data.isSystem) ) { // body and html view.webContents.executeJavaScript(` document.body.setAttribute('data-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark'); `); if (data.isSystem) { view.webContents.executeJavaScript( `document.body.setAttribute('arco-theme', 'dark');`, ); } } }; ================================================ FILE: electron/mapi/manager/main.ts ================================================ import { BrowserWindow, ipcMain } from "electron"; import { ActionRecord, ActionTypeEnum, FilePluginRecord, LaunchRecord, PluginEnv, } from "../../../src/types/Manager"; import { t } from "../../config/lang"; import { Permissions } from "../../lib/permission"; import { Page } from "../../page"; import { AppsMain } from "../app/main"; import { AppRuntime } from "../env"; import ProtocolMain from "../protocol/main"; import { ManagerAutomation } from "./automation"; import { ManagerBackend } from "./backend"; import { ManagerClipboard } from "./clipboard"; import { ManagerConfig } from "./config/config"; import { ManagerEditor } from "./editor"; import { ManagerHotkey } from "./hotkey"; import { Manager } from "./manager"; import { ManagerPlugin } from "./plugin"; import { ManagerPluginEvent } from "./plugin/event"; import { PluginHttp } from "./plugin/http"; import { PluginHttpMCP } from "./plugin/httpMCP"; import { PluginLog } from "./plugin/log"; import { ManagerSystem } from "./system"; import { ManagerSystemPluginFile } from "./system/plugin/file"; import { ManagerPluginStore } from "./system/plugin/store/index"; import { PluginContext, SearchQuery } from "./type"; import { ManagerWindow } from "./window"; const init = () => { ManagerClipboard.init().then(); ManagerEditor.init().then(); PluginHttp.init().then(); }; const ready = () => { Permissions.checkAccessibilityAccess().then((enable) => { if (enable) { ManagerHotkey.init(); } else { Page.open("setup").then(); } }); Permissions.checkScreenCaptureAccess().then((enable) => { if (enable) { ManagerAutomation.init(); } else { Page.open("setup").then(); } }); ManagerEditor.ready().then(); }; const destroy = () => { ManagerClipboard.monitorStop(); ManagerHotkey.destroy(); }; ipcMain.handle("manager:getConfig", async (event) => { return await ManagerConfig.get(); }); ipcMain.handle("manager:setConfig", async (event, config) => { return await ManagerConfig.save(config); }); ipcMain.handle("manager:getMcpServer", async (event) => { return await PluginHttp.getMcpServer(); }); ipcMain.handle("manager:getMcpInfo", async (event) => { const toolsResult = await PluginHttpMCP["tools/list"]({}); return { tools: toolsResult.tools, }; }); ipcMain.handle("manager:isShown", async (event) => { return await ManagerPluginEvent.isMainWindowShown(null, null); }); ipcMain.handle("manager:show", async (event) => { return await ManagerPluginEvent.showMainWindow(null, null); }); ipcMain.handle("manager:hide", async (event) => { return await ManagerPluginEvent.hideMainWindow(null, null); }); ipcMain.handle("manager:getClipboardContent", async (event) => { return await ManagerClipboard.getClipboardContent(); }); ipcMain.handle("manager:getClipboardChangeTime", async (event) => { return ManagerClipboard.lastChangeTimestamp; }); ipcMain.handle("manager:getSelectedContent", async (event) => { return Manager.selectedContent; }); ipcMain.handle("manager:listPlugin", async (event, option?: {}) => { return await Manager.listPlugin(); }); ipcMain.handle( "manager:installPlugin", async (event, fileOrPath: string, option?: {}) => { return await ManagerPlugin.installFromFileOrDir(fileOrPath); }, ); ipcMain.handle( "manager:refreshInstallPlugin", async (event, pluginName: string, option?: {}) => { return await ManagerPlugin.refreshInstall(pluginName); }, ); ipcMain.handle( "manager:uninstallPlugin", async (event, pluginName: string, option?: {}) => { // console.log('manager:uninstallPlugin', pluginName) return await ManagerPlugin.uninstall(pluginName); }, ); ipcMain.handle( "manager:getPluginInstalledVersion", async (event, pluginName: string, option?: {}) => { return await ManagerPlugin.getPluginInstalledVersion(pluginName); }, ); ipcMain.handle( "manager:listDisabledActionMatch", async (event, option?: {}) => { return await ManagerConfig.listDisabledActionMatch(); }, ); ipcMain.handle( "manager:toggleDisabledActionMatch", async ( event, pluginName: string, actionName: string, matchName: string, option?: {}, ) => { return await ManagerConfig.toggleDisabledActionMatch( pluginName, actionName, matchName, ); }, ); ipcMain.handle("manager:listPinAction", async (event, option?: {}) => { return await ManagerConfig.listPinAction(); }); ipcMain.handle( "manager:togglePinAction", async (event, pluginName: string, actionName: string, option?: {}) => { return await ManagerConfig.togglePinAction(pluginName, actionName); }, ); ipcMain.handle( "manager:showLog", async (event, pluginName: string, option?: {}) => { await ManagerPluginEvent.logShow( { _plugin: { name: pluginName, }, } as any, {}, ); }, ); ipcMain.handle("manager:clearCache", async (event, option?: {}) => { await ManagerConfig.clearCache(); await ManagerSystem.clearCache(); await ManagerPlugin.clearCache(); }); ipcMain.handle("manager:hotKeyWatch", async (event, option?: {}) => { await ManagerHotkey.watch(); }); ipcMain.handle("manager:hotKeyUnwatch", async (event, option?: {}) => { await ManagerHotkey.unwatch(); }); ipcMain.handle("manager::listAction", async (event) => { return Manager.listAction(); }); const mergeViewActionRuntime = async (actions: ActionRecord[]) => { for (const a of actions) { const plugin = await Manager.getPlugin(a.pluginName); const { nodeIntegration, preloadBase, mainView } = await ManagerPlugin.getInfo(plugin); a.runtime.view = { nodeIntegration, preloadBase, mainView, showViewDevTools: false, heightView: 100, }; // console.log('mergeViewActionRuntime', plugin.development) if ( plugin.development && plugin.development.env === PluginEnv.DEV && plugin.development.showViewDevTools ) { a.runtime.view.showViewDevTools = true; } if (plugin.setting && plugin.setting.heightView) { a.runtime.view.heightView = plugin.setting.heightView; } for (const k of ["preloadBase", "mainView"]) { if ( a.runtime.view[k] && !a.runtime.view[k].startsWith("file://") && !a.runtime.view[k].startsWith("http://") ) { a.runtime.view[k] = "file://" + a.runtime.view[k]; } } } }; ipcMain.handle( "manager:searchFastPanelAction", async (event, query: SearchQuery, option?: {}) => { query = Object.assign( { keywords: "", currentFiles: [], currentImage: "", currentText: "", activeWindow: Manager.activeWindow, }, query, ); // console.log('manager:searchFastPanelAction', query) const request = Manager.createSearchRequest(query); const result = { id: request.id, matchActions: [] as ActionRecord[], viewActions: [] as ActionRecord[], }; const actions = await Manager.listAction(request); const actionFullNameMap = new Map(); for (const a of actions) { actionFullNameMap.set(a.fullName, a); } const uniqueRemover = new Set(); result.matchActions = [ ...(await Manager.matchActions(uniqueRemover, actions, query)), ...(await Manager.searchActions(uniqueRemover, actions, query)), ...(await Manager.pinActions( uniqueRemover, actionFullNameMap, query, )), ...(await Manager.historyActions( uniqueRemover, actionFullNameMap, query, )), ]; result.viewActions = result.matchActions.filter( (a) => a.type === ActionTypeEnum.VIEW && a.data?.showFastPanel, ); result.matchActions = result.matchActions.filter( (a) => a.type !== ActionTypeEnum.VIEW, ); await mergeViewActionRuntime(result.viewActions); return result; }, ); ipcMain.handle( "manager:searchAction", async (event, query: SearchQuery, option?: {}) => { query = Object.assign( { keywords: "", currentFiles: [], currentImage: "", currentText: "", activeWindow: Manager.activeWindow, }, query, ); // console.log('manager:searchAction', query) const request = Manager.createSearchRequest(query); const result = { id: request.id, detachWindowActions: [], searchActions: [], matchActions: [], viewActions: [], historyActions: [], pinActions: [], }; // 所有已知的动作 const actions = await Manager.listAction(request); const actionFullNameMap = new Map(); for (const a of actions) { actionFullNameMap.set(a.fullName, a); } // Files.write('actions.json', JSON.stringify(actions)) const uniqueRemover = new Set(); if (!query.keywords) { result.detachWindowActions = await Manager.detachWindowActions( uniqueRemover, actionFullNameMap, ); } result.searchActions = await Manager.searchActions( uniqueRemover, actions, query, ); result.matchActions = await Manager.matchActions( uniqueRemover, actions, query, ); result.viewActions = [ ...result.searchActions.filter( (a) => a.type === ActionTypeEnum.VIEW && a.data?.showMainPanel, ), ...result.matchActions.filter( (a) => a.type === ActionTypeEnum.VIEW && a.data?.showMainPanel, ), ]; result.searchActions = result.searchActions.filter( (a) => a.type !== ActionTypeEnum.VIEW, ); result.matchActions = result.matchActions.filter( (a) => a.type !== ActionTypeEnum.VIEW, ); if (!query.keywords) { result.historyActions = await Manager.historyActions( uniqueRemover, actionFullNameMap, query, ); result.pinActions = await Manager.pinActions( new Set(), actionFullNameMap, query, ); } const pinedSet = await ManagerConfig.getPinedActionSet(); result.searchActions.forEach((a) => { a.runtime.isPined = pinedSet.has(`${a.pluginName}/${a.name}`); }); result.matchActions.forEach((a) => { a.runtime.isPined = pinedSet.has(`${a.pluginName}/${a.name}`); }); result.historyActions.forEach((a) => { a.runtime.isPined = pinedSet.has(`${a.pluginName}/${a.name}`); }); result.pinActions.forEach((a) => { a.runtime.isPined = true; }); await mergeViewActionRuntime(result.viewActions); return result; }, ); ipcMain.handle( "manager:listDetachWindowActions", async (event, option?: {}) => { const actions = await Manager.listAction(); const actionFullNameMap = new Map(); for (const a of actions) { actionFullNameMap.set(a.fullName, a); } const uniqueRemover = new Set(); const result = await Manager.detachWindowActions( uniqueRemover, actionFullNameMap, ); await mergeViewActionRuntime(result); return result; }, ); ipcMain.handle( "manager:subInputChange", async (event, keywords: string, option?: {}) => { const senderWindow = BrowserWindow.fromWebContents(event.sender); await ManagerWindow.subInputChange(senderWindow, keywords); }, ); ipcMain.handle( "manager:openPlugin", async (event, pluginName: string, option?: {}) => { await Manager.openPlugin(pluginName); }, ); ipcMain.handle("manager:openAction", async (event, action: ActionRecord) => { await Manager.openAction(action); }); ipcMain.handle("manager:openActionCode", async (event, id: string) => { await ManagerWindow.actionCodeExecute(id, null); }); ipcMain.handle("manager:searchActionCode", async (event, keywords: string) => { await ManagerWindow.actionCodeExecute(null, keywords); }); ipcMain.handle( "manager:openActionWindow", async (event, type: "open" | "close", action: ActionRecord) => { await Manager.openActionWindow(type, action); }, ); ipcMain.handle("manager:closeMainPlugin", async (event, option?: {}) => { await ManagerWindow.close(null); }); ipcMain.handle("manager:openMainPluginDevTools", async (event, option?: {}) => { await ManagerWindow.openMainPluginDevTools(); }); ipcMain.handle("manager:openMainPluginLog", async (event, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerPluginEvent.logShow(view, {}); }); ipcMain.handle("manager:detachPlugin", async (event, option) => { await ManagerWindow.detach(); }); ipcMain.handle( "manager:toggleDetachPluginAlwaysOnTop", async (event, alwaysOnTop: boolean, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); return ManagerWindow.toggleDetachPluginAlwaysOnTop( view, alwaysOnTop, option, ); }, ); ipcMain.handle( "manager:setDetachPluginZoom", async (event, zoom: number, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerWindow.setDetachPluginZoom(view, zoom, option); await ManagerConfig.setPluginConfigItem( view._plugin.name, "zoom", zoom, ); }, ); ipcMain.handle( "manager:firePluginMoreMenuClick", async (event, name: string, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerWindow.firePluginMoreMenuClick(view, name, option); }, ); ipcMain.handle( "manager:fireDetachOperateClick", async (event, name: string, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerWindow.fireDetachOperateClick(view, name, option); }, ); ipcMain.handle("manager:closeDetachPlugin", async (event) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerWindow.closeDetachPlugin(view); }); ipcMain.handle( "manager:openDetachPluginDevTools", async (event, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerWindow.openDetachPluginDevTools(view); }, ); ipcMain.handle("manager:openDetachPluginLog", async (event, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerPluginEvent.logShow(view, {}); }); ipcMain.handle( "manager:setPluginAutoDetach", async (event, autoDetach: boolean, option?: {}) => { const view = ManagerWindow.getViewByWebContents(event.sender); await ManagerConfig.setPluginConfigItem( view._plugin.name, "autoDetach", autoDetach, ); }, ); ipcMain.handle( "manager:getPluginConfig", async (event, pluginName: string, option?: {}) => { return await ManagerConfig.getPluginConfig(pluginName); }, ); ipcMain.handle("manager:listFilePluginRecords", async (event, option?: {}) => { return await ManagerSystemPluginFile.list(); }); ipcMain.handle( "manager:updateFilePluginRecords", async (event, records: FilePluginRecord[], option?: {}) => { return await ManagerSystemPluginFile.update(records); }, ); ipcMain.handle("manager:listLaunchRecords", async (event, option?: {}) => { return await ManagerConfig.listLaunch(); }); ipcMain.handle( "manager:updateLaunchRecords", async (event, records: LaunchRecord[], option?: {}) => { return await ManagerConfig.updateLaunch(records); }, ); ipcMain.handle( "manager:storeInstall", async (event, pluginName: string, option?: {}) => { return await ManagerPluginStore.install(pluginName, option); }, ); ProtocolMain.register("open", async (params) => { const pluginName = params.pluginName; const pluginVersion = params.pluginVersion; const autoInstall = params.autoInstall; const plugin = await Manager.getPlugin(pluginName); if (!plugin) { if (autoInstall) { const loading = AppsMain.loading(t("plugin.installing"), { percentAuto: true, }); try { await ManagerPluginStore.install(pluginName, { version: pluginVersion, }); } catch (e) { AppsMain.toast(t("plugin.installFailed"), { status: "error", }); return; } finally { loading.close(); } AppsMain.toast(t("plugin.opening"), { status: "success", }); await Manager.openPlugin(pluginName); } else { AppsMain.toast(t("plugin.notExist", { name: pluginName }), { status: "error", }); } } else { AppsMain.toast(t("plugin.opening"), { status: "success", }); await Manager.openPlugin(pluginName); } }); ipcMain.handle( "manager:storePublish", async (event, pluginName: string, option?: {}) => { return await ManagerPluginStore.publish(pluginName, option); }, ); ipcMain.handle( "manager:storePublishInfo", async (event, pluginName: string, option?: {}) => { return await ManagerPluginStore.publishInfo(pluginName, option); }, ); ipcMain.handle( "manager:storeInstallingInfo", async (event, pluginName: string, option?: {}) => { return await ManagerPluginStore.storeInstallingInfo(pluginName); }, ); ipcMain.handle( "manager:clipboardList", async ( event, option?: { limit?: number; }, ) => { option = Object.assign( { limit: -1, }, option, ); return await ManagerClipboard.list(option.limit); }, ); ipcMain.handle("manager:clipboardClear", async (event, option?: {}) => { return await ManagerClipboard.clear(); }); ipcMain.handle( "manager:clipboardDelete", async (event, timestamp: number, option?: {}) => { return await ManagerClipboard.delete(timestamp); }, ); ipcMain.handle("manager:historyClear", async (event, option?: {}) => { return await ManagerConfig.clearHistoryAction(); }); ipcMain.handle( "manager:historyDelete", async (event, pluginName: string, actionName: string, option?: {}) => { return await ManagerConfig.deleteHistoryAction(pluginName, actionName); }, ); const getViewByEvent = (event) => { let view = ManagerWindow.getViewByWebContents(event.sender); if (!view) { try { const userAgent = event.sender.getUserAgent(); const match = userAgent.match(/PluginAction\/([^/]+)\/([^/]+)$/); if (match) { const pluginName = match[1]; const actionName = match[2]; view = { _plugin: Manager.getPluginSync(pluginName), } as PluginContext; } } catch (e) {} } return view; }; ipcMain.on("FocusAny.Event", async (_event, payload: any) => { const view = getViewByEvent(_event); const { id, event, data } = payload; // console.log('FocusAny.Event', {id, event, data, view}) const plugin = view._plugin; const result = await ManagerBackend.run(plugin, "event", event, data, { rejectIfError: true, }); const resultEvent = `FocusAny.Event.${id}`; view.webContents.send(resultEvent, result); }); ipcMain.on( "FocusAny.Plugin", ( event, payload: { type: string; data: any; }, ) => { const view = getViewByEvent(event); const { type, data } = payload; ManagerPluginEvent[type](view, data) .then((result) => { event.returnValue = result; }) .catch((e) => { event.returnValue = e; PluginLog.error(view._plugin.name, `ApiError.${type}`, { error: "" + e, data, }); }); }, ); ipcMain.handle( "FocusAny.Plugin.Async", async ( event, payload: { type: string; data: any; }, ) => { const view = getViewByEvent(event); const { type, data } = payload; try { return await ManagerPluginEvent[type](view, data); } catch (e) { PluginLog.error(view._plugin.name, `ApiError.${type}`, { error: "" + e, data, }); throw e; } }, ); ipcMain.on("SendTo", (event, winId: number, type: string, ...args: any) => { // console.log('SendTo', event.sender.getType(), event.sender.id, {winId, type, payload}) BrowserWindow.getAllWindows().forEach((w) => { if (w === AppRuntime.fastPanelWindow) { return; } if (w === AppRuntime.mainWindow) { for (let v of w.getBrowserViews()) { if (v.webContents.id === winId) { v.webContents.send(type, event.sender.id, ...args); } } } else { if (w.webContents.id === winId) { w.webContents.send(type, event.sender.id, ...args); } } }); }); export default { init, ready, destroy, }; ================================================ FILE: electron/mapi/manager/manager.ts ================================================ import { ActionMatchFile, ActionMatchKey, ActionMatchRegex, ActionMatchText, ActionMatchTypeEnum, ActionMatchWindow, ActionRecord, ActionTypeEnum, ActiveWindow, PluginRecord, SelectedContent, } from "../../../src/types/Manager"; import { ManagerSystem } from "./system"; import { ManagerPlugin } from "./plugin"; import { ManagerConfig } from "./config/config"; import { SearchQuery } from "./type"; import { PinyinUtil } from "../../lib/pinyin-util"; import { exec } from "child_process"; import { ManagerWindow } from "./window"; import { ManagerCode } from "./code"; import { ManagerBackend } from "./backend"; import { ReUtil, StrUtil } from "../../lib/util"; import { Events } from "../event/main"; import { ManagerEditor } from "./editor"; import { cloneDeep } from "lodash"; type SearchRequest = { id: string; query: SearchQuery; }; let plugins: PluginRecord[] = []; export const Manager = { selectedContent: null as SelectedContent | null, activeWindow: null as ActiveWindow | null, searchRequests: [] as SearchRequest[], createSearchRequest(query: SearchQuery) { const id = StrUtil.randomString(8); if (this.searchRequests.length > 3) { this.searchRequests.shift(); } const request = { id, query, }; this.searchRequests.push(request); return request; }, getSearchRequestQuery(id: string) { for (const s of this.searchRequests) { if (s.id === id) { return s.query; } } return null; }, async openPlugin(pluginName: string) { const plugin = await this.getPlugin(pluginName); if (!plugin) { throw "PluginNotExists"; } if (!plugin.actions || !plugin.actions.length) { throw "PluginNoActions"; } for (const a of plugin.actions) { if (a.type === ActionTypeEnum.WEB) { await this.openAction(a); return; } } }, async openActionWindow(type: "open" | "close", action: ActionRecord) { await ManagerWindow.detachWindowOperate(type, action); }, async openAction(action: ActionRecord) { const plugin = await Manager.getPlugin(action.pluginName); if (!plugin) { return; } if (!action.runtime) { action.runtime = { searchScore: 0, searchTitleMatched: "", match: null, }; } switch (action.type) { case ActionTypeEnum.COMMAND: exec(action.data.command); break; case ActionTypeEnum.WEB: await ManagerWindow.open(plugin, action); break; case ActionTypeEnum.CODE: await ManagerCode.execute(plugin, action); break; case ActionTypeEnum.BACKEND: await ManagerBackend.runAction(plugin, action); break; } if (action.trackHistory) { await ManagerConfig.addHistoryAction(plugin, action); } }, async getPlugin(name: string) { for (let p of await this.listPlugin()) { if (p.name === name) { return p; } } return null; }, getPluginSync(name: string) { for (let p of plugins) { if (p.name === name) { return p; } } return null; }, async listPlugin() { plugins = [ ...(await ManagerSystem.list()), ...(await ManagerPlugin.list()), ]; const customActions = await ManagerConfig.getCustomAction(); for (const p of plugins) { if (!(p.name in customActions)) { continue; } p.actions = p.actions.concat(customActions[p.name]); } return plugins; }, async listAction(request?: SearchRequest) { let actions: ActionRecord[] = [ ...(await ManagerSystem.listAction()), ...(await ManagerPlugin.listAction()), ]; const customActions = await ManagerConfig.getCustomAction(); for (const customAction of Object.values(customActions)) { actions = actions.concat(customAction); } for (let a of actions) { a.runtime = { searchScore: 0, searchTitleMatched: "", match: null, requestId: request ? request.id : null, }; } return actions; }, async searchOneAction( keywordsOrAction: string | string[], query: SearchQuery, ) { const request = this.createSearchRequest(query); query = Object.assign( { keywords: "", currentFiles: [], currentImage: "", currentText: "", }, query, ); const actions = await this.listAction(request); let action: ActionRecord = null; if (typeof keywordsOrAction === "string") { const uniqueRemover = new Set(); const results = await this.searchActions(uniqueRemover, actions, { ...query, keywords: keywordsOrAction, }); if (results.length > 0) { action = results[0]; } } else { const fullName = keywordsOrAction.join("/"); for (let a of actions) { if (a.fullName === fullName) { action = a; break; } } } return action; }, async matchActionSimple(query: SearchQuery): Promise { const request = this.createSearchRequest(query); query = Object.assign( { keywords: "", currentFiles: [], currentImage: "", currentText: "", activeWindow: this.activeWindow, }, query, ); const actions = await this.listAction(request); const uniqueRemover = new Set(); return await this.matchActions(uniqueRemover, actions, query); }, async searchActions( uniqueRemover: Set, actions: ActionRecord[], query: SearchQuery, ): Promise { let results = []; if (!query.keywords) { return results; } for (const a of actions) { if (!a.matches || uniqueRemover.has(a.fullName)) { continue; } let searchScoreMax = 0; let runtimeSearchTitleMatched = ""; let runtimeMatch = null; for (const m of a.matches) { if (m.type === ActionMatchTypeEnum.TEXT) { if ( "minLength" in m && query.keywords.length < m.minLength ) { continue; } if ( "maxLength" in m && query.keywords.length > m.maxLength ) { continue; } const textMatch = PinyinUtil.match( (m as ActionMatchText).text, query.keywords, ); if ( textMatch.matched && textMatch.similarity > searchScoreMax ) { searchScoreMax = textMatch.similarity; runtimeSearchTitleMatched = textMatch.inputMark; runtimeMatch = m; } } else if (m.type === ActionMatchTypeEnum.KEY) { if ((m as ActionMatchKey).key === query.keywords) { searchScoreMax = 1; runtimeSearchTitleMatched = PinyinUtil.mark( query.keywords, ); runtimeMatch = m; } } } // console.log('searchScoreMax', a.name, searchScoreMax, a.runtime.searchScore > 0) if (searchScoreMax > 0) { a.runtime.searchScore = searchScoreMax; a.runtime.searchTitleMatched = runtimeSearchTitleMatched; a.runtime.match = runtimeMatch; results.push(a); uniqueRemover.add(a.fullName); } } // sort by similarity results = results.sort((a, b) => { return b.runtime.searchScore - a.runtime.searchScore; }); return results; }, async matchActions( uniqueRemover: Set, actions: ActionRecord[], query: SearchQuery, ): Promise { let results = []; if ( !query.keywords && !query.currentImage && !query.currentFiles.length && !query.currentText && !query.activeWindow ) { return results; } const keywords = query.currentText || query.keywords; for (const a of actions) { if (!a.matches || uniqueRemover.has(a.fullName)) { continue; } let searchScoreMax = 0; let runtimeSearchTitleMatched = ""; let runtimeMatch = null; let matchFiles = []; for (const m of a.matches) { if (m.type === ActionMatchTypeEnum.REGEX) { if (!keywords) { continue; } if ("minLength" in m && keywords.length < m.minLength) { continue; } if ("maxLength" in m && keywords.length > m.maxLength) { continue; } if (ReUtil.match((m as ActionMatchRegex).regex, keywords)) { searchScoreMax = 1; runtimeSearchTitleMatched = (m as ActionMatchRegex).title || a.title; runtimeMatch = m; break; } } else if (m.type === ActionMatchTypeEnum.FILE) { let files = query.currentFiles; if (files.length <= 0) { continue; } // console.log('file', JSON.stringify({m, files}, null, 2)) if ("filterFileType" in m) { if (m.filterFileType === "file") { files = files.filter((f) => f.isFile); } else if (m.filterFileType === "directory") { files = files.filter((f) => f.isDirectory); } } if ("filterExtensions" in m) { files = files.filter( (f) => f.isFile && ( m as ActionMatchFile ).filterExtensions.includes(f.fileExt), ); } if ("minCount" in m && files.length < m.minCount) { continue; } if ("maxCount" in m && files.length > m.maxCount) { continue; } if (files.length <= 0) { continue; } searchScoreMax = 1; runtimeSearchTitleMatched = (m as ActionMatchFile).title || a.title; runtimeMatch = m; matchFiles = files; break; } else if (m.type === ActionMatchTypeEnum.IMAGE) { const image = query.currentImage; if (!image) { continue; } searchScoreMax = 1; runtimeSearchTitleMatched = (m as ActionMatchFile).title || a.title; runtimeMatch = m; } else if (m.type === ActionMatchTypeEnum.WINDOW) { const activeWindow = query.activeWindow; if (!activeWindow) { continue; } if ( (m as ActionMatchWindow).nameRegex && !ReUtil.match( (m as ActionMatchWindow).nameRegex, activeWindow.name, ) ) { continue; } if ( (m as ActionMatchWindow).titleRegex && !ReUtil.match( (m as ActionMatchWindow).titleRegex, activeWindow.title, ) ) { continue; } if ((m as ActionMatchWindow).attrRegex) { let pass = true; for (const key in (m as ActionMatchWindow).attrRegex) { if ( !ReUtil.match( (m as ActionMatchWindow).attrRegex[key], activeWindow.attr[key], ) ) { pass = false; break; } } if (!pass) { continue; } } searchScoreMax = 1; runtimeSearchTitleMatched = a.title; runtimeMatch = m; break; } else if (m.type === ActionMatchTypeEnum.EDITOR) { let files = query.currentFiles; if (files.length <= 0) { continue; } // console.log('file', JSON.stringify({m, files}, null, 2)) if ("extensions" in m) { files = files.filter( (f) => f.isFile && (m as ActionMatchEditor).extensions.includes( f.fileExt, ), ); } if ("fadTypes" in m) { files = await ManagerEditor.filterFadType( files, (m as ActionMatchEditor).fadTypes, ); } if (files.length <= 0) { continue; } searchScoreMax = 1; runtimeSearchTitleMatched = a.title; runtimeMatch = m; matchFiles = files; break; } } // console.log('searchScoreMax', a.name, searchScoreMax, a.runtime.searchScore > 0) if (searchScoreMax > 0) { a.runtime.searchScore = searchScoreMax; a.runtime.searchTitleMatched = runtimeSearchTitleMatched; a.runtime.match = runtimeMatch; a.runtime.matchFiles = matchFiles; results.push(a); uniqueRemover.add(a.fullName); } } // sort by similarity results = results.sort((a, b) => { return b.runtime.searchScore - a.runtime.searchScore; }); return results; }, async detachWindowActions( uniqueRemover: Set, actionFullNameMap: Map, ) { const results = []; const pluginCount = {}; for (const win of ManagerWindow.listDetachWindows()) { let actionWeb = null; for (const a of win._plugin.actions) { if (a.type === ActionTypeEnum.WEB) { actionWeb = a; break; } } if (win._type && win._type === "callPage") { continue; } if (!actionWeb) { continue; } const fullName = actionWeb.pluginName + "/" + actionWeb.name; if (actionFullNameMap.has(fullName)) { const action = actionFullNameMap.get(fullName); const actionClone = cloneDeep(action); actionClone.runtime.windowId = win.id; if (pluginCount[actionWeb.pluginName]) { pluginCount[actionWeb.pluginName]++; } else { pluginCount[actionWeb.pluginName] = 1; } actionClone.runtime.windowIndex = pluginCount[actionWeb.pluginName]; results.push(actionClone); uniqueRemover.add(fullName); } } for (const r of results) { r.runtime.windowCount = results.filter( (a) => a.pluginName === r.pluginName, ).length; } return results; }, async historyActions( uniqueRemover: Set, actionFullNameMap: Map, query: SearchQuery, ) { const historyActions = await ManagerConfig.getHistoryAction(); const results = []; for (const h of historyActions) { const fullName = h.pluginName + "/" + h.actionName; if (uniqueRemover.has(fullName)) { continue; } if (actionFullNameMap.has(fullName)) { results.push(actionFullNameMap.get(fullName)); uniqueRemover.add(fullName); } } return results; }, async pinActions( uniqueRemover: Set, actionFullNameMap: Map, query: SearchQuery, ) { const pinActions = await ManagerConfig.listPinAction(); const results: ActionRecord[] = []; for (const p of pinActions) { const fullName = p.pluginName + "/" + p.actionName; if (uniqueRemover.has(fullName)) { continue; } if (actionFullNameMap.has(fullName)) { results.push(actionFullNameMap.get(fullName)); uniqueRemover.add(fullName); } } return results; }, async sendBroadcast(pluginName: string, type: string, data: any) { for (const view of ManagerWindow.listBrowserViews()) { if (view._plugin && view._plugin.name === pluginName) { Events.sendRaw(view.webContents, "BROADCAST", { type, data, }); } } }, async setNotice( notice: | { text: string; type?: "info" | "error" | "success"; duration?: number; } | string, ) { if (typeof notice === "string") { notice = { text: notice }; } notice = Object.assign( { text: "", type: "info", duration: 0, }, notice, ); Events.broadcast("Notice", notice, { limit: true, scopes: ["main"], }); }, }; ================================================ FILE: electron/mapi/manager/plugin/colorPicker.ts ================================================ import { FileType, screen as nutScreen, Region } from "@nut-tree-fork/nut-js"; import { BrowserWindow, ipcMain, nativeImage, screen } from "electron"; import { uIOhook, UiohookKey } from "uiohook-napi"; import { t } from "../../../config/lang"; import { isLinux, isMac, isWin } from "../../../lib/env"; import { AppsMain } from "../../app/main"; import { Files } from "../../file/main"; let currentPromise: Promise | null = null; export const colorPicker = async (): Promise => { if (currentPromise) { return currentPromise; } currentPromise = new Promise((resolve) => { let magnifierWindow: BrowserWindow | null = null; let isPicking = true; let updating = false; let currentColor: string = "#FFFFFF"; let bitmaps: { display: any; bitmap: Uint8Array; size: { width: number; height: number }; scaleFactor: number; originalPhysicalX: number; originalPhysicalY: number; clampedPhysicalX: number; clampedPhysicalY: number; }[] = []; const cleanup = () => { if (magnifierWindow) { magnifierWindow.close(); magnifierWindow = null; } ipcMain.off("copy-color", copyHandler); }; const copyHandler = () => { AppsMain.setClipboardText(currentColor); AppsMain.toast(t("plugin.colorCopied", { color: currentColor })); isPicking = false; resolve(); cleanup(); }; ipcMain.on("copy-color", copyHandler); // HTML for magnifier const copyShortcut = isMac ? "Cmd+Shift+C" : "Ctrl+Shift+C"; const html = `
#FFFFFF
${t("plugin.colorCopyShortcut", { shortcut: copyShortcut })}
${t("plugin.exitEsc")}
`; // Create magnifier window magnifierWindow = new BrowserWindow({ width: 350, height: 120, frame: false, transparent: false, alwaysOnTop: true, skipTaskbar: true, resizable: false, show: false, webPreferences: { nodeIntegration: true, contextIsolation: false, }, }); magnifierWindow.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(html)}`, ); magnifierWindow.once("ready-to-show", async () => { magnifierWindow!.on("closed", () => { uIOhook.off("mousemove", mouseMoveCallback); uIOhook.off("keydown", keyDownCallback); }); const totalWidth = await nutScreen.width(); const totalHeight = await nutScreen.height(); const displays = screen.getAllDisplays(); const screenshots = await Promise.all( displays.map(async (display) => { try { const scaleFactor = display.scaleFactor; const physicalX = Math.round( display.bounds.x * scaleFactor, ); const physicalY = Math.round( display.bounds.y * scaleFactor, ); const physicalWidth = Math.round( display.bounds.width * scaleFactor, ); const physicalHeight = Math.round( display.bounds.height * scaleFactor, ); const originalPhysicalX = physicalX; const originalPhysicalY = physicalY; const clampedPhysicalX = Math.max(0, physicalX); const clampedPhysicalY = Math.max(0, physicalY); const clampedPhysicalWidth = Math.min( physicalWidth, totalWidth - clampedPhysicalX, ); const clampedPhysicalHeight = Math.min( physicalHeight, totalHeight - clampedPhysicalY, ); const region = new Region( clampedPhysicalX, clampedPhysicalY, clampedPhysicalWidth, clampedPhysicalHeight, ); const capturePath = await ( nutScreen.captureRegion as any )( await Files.tempName(), region, FileType.PNG, await Files.tempRoot(), ); // console.log('capturePath', capturePath); const image = nativeImage.createFromPath(capturePath); Files.deletes(capturePath).then(); const bitmap = image.getBitmap() as any; const size = image.getSize(); return { display, bitmap, size, scaleFactor, originalPhysicalX, originalPhysicalY, clampedPhysicalX, clampedPhysicalY, }; } catch (error) { console.error("Error capturing display:", error); return null; } }), ); bitmaps = screenshots.filter( ( item, ): item is { display: any; bitmap: Uint8Array; size: { width: number; height: number }; scaleFactor: number; originalPhysicalX: number; originalPhysicalY: number; clampedPhysicalX: number; clampedPhysicalY: number; } => Boolean(item), ); await updateMagnifier(); magnifierWindow!.show(); }); const updateMagnifier = async () => { if (!isPicking || updating || bitmaps.length === 0) return; updating = true; try { const mousePos = screen.getCursorScreenPoint(); const display = screen.getDisplayNearestPoint(mousePos); const bitmapData = bitmaps.find( (b) => b.display.id === display.id, ); if (!bitmapData) return; const { bitmap, size, scaleFactor, originalPhysicalX, originalPhysicalY, clampedPhysicalX, clampedPhysicalY, } = bitmapData; const offsetX = Math.round( (mousePos.x - display.bounds.x) * scaleFactor + (originalPhysicalX - clampedPhysicalX), ); const offsetY = Math.round( (mousePos.y - display.bounds.y) * scaleFactor + (originalPhysicalY - clampedPhysicalY), ); const colors: number[] = []; for (let dy = -9; dy <= 10; dy++) { for (let dx = -9; dx <= 10; dx++) { const px = offsetX + dx; const py = offsetY + dy; if ( px < 0 || px >= size.width || py < 0 || py >= size.height ) { colors.push(255, 255, 255, 255); // white } else { const index = (py * size.width + px) * 4; const r = bitmap[index + 2]; const g = bitmap[index + 1]; const b = bitmap[index]; const a = bitmap[index + 3]; colors.push(r, g, b, a); } } } const imageData = new Uint8ClampedArray(colors); magnifierWindow!.webContents.executeJavaScript(` window.electronAPI.updateMagnifier(${JSON.stringify(Array.from(imageData))}); `); // Get current color at mouse position const centerIndex = (10 * 21 + 10) * 4; // Center pixel const r = colors[centerIndex]; const g = colors[centerIndex + 1]; const b = colors[centerIndex + 2]; const hex = "#" + r.toString(16).padStart(2, "0") + g.toString(16).padStart(2, "0") + b.toString(16).padStart(2, "0"); currentColor = hex.toUpperCase(); magnifierWindow!.webContents.executeJavaScript(` window.electronAPI.updateColor('${hex}'); `); // Position window in top-right or top-left based on mouse position const windowBounds = magnifierWindow!.getBounds(); const isMouseInTopRight = mousePos.x > display.bounds.x + display.bounds.width * 0.75 && mousePos.y < display.bounds.y + display.bounds.height * 0.25; let x, y; if (isMouseInTopRight) { x = display.bounds.x; y = display.bounds.y; } else { x = display.bounds.x + display.bounds.width - windowBounds.width; y = display.bounds.y; } magnifierWindow!.setPosition(x, y); } catch (error) { console.error("Error updating magnifier:", error); } finally { updating = false; } }; // Listen to mouse events const mouseMoveCallback = () => { updateMagnifier(); }; const keyDownCallback = (event: any) => { if (!isPicking) return; if ( (isMac && event.metaKey && event.shiftKey && event.keycode === UiohookKey.C) || ((isWin || isLinux) && event.ctrlKey && event.shiftKey && event.keycode === UiohookKey.C) ) { AppsMain.setClipboardText(currentColor); AppsMain.toast( t("plugin.colorCopied", { color: currentColor }), ); isPicking = false; resolve(); cleanup(); currentPromise = null; } else if (event.keycode === UiohookKey.Escape) { // ESC to exit isPicking = false; resolve(); // Or currentColor, but exit without copying cleanup(); currentPromise = null; } }; uIOhook.on("mousemove", mouseMoveCallback); uIOhook.on("keydown", keyDownCallback); // Initial update mouseMoveCallback(); }); }; ================================================ FILE: electron/mapi/manager/plugin/event.ts ================================================ import { app, BrowserView, BrowserWindow, clipboard, dialog, nativeImage, Notification, screen, shell, } from "electron"; import fs from "fs"; import { AppConfig } from "../../../../src/config"; import { CommonConfig } from "../../../config/common"; import { t } from "../../../config/lang"; import { WindowConfig } from "../../../config/window"; import { isLinux, isMac, isWin, platformArch, platformName, platformUUID, } from "../../../lib/env"; import { EncodeUtil } from "../../../lib/util"; import { Page } from "../../../page"; import { PagePayment } from "../../../page/payment"; import { PageUser } from "../../../page/user"; import { AppPosition } from "../../app/lib/position"; import { AppsMain } from "../../app/main"; import { AppRuntime } from "../../env"; import { Files } from "../../file/main"; import { KVDBMain } from "../../kvdb/main"; import { DBError } from "../../kvdb/types"; import { Log } from "../../log/main"; import User, { UserApi } from "../../user/main"; import { ManagerAutomation } from "../automation"; import { ManagerClipboard } from "../clipboard"; import { getClipboardFiles, setClipboardFiles, } from "../clipboard/clipboardFiles"; import { ManagerConfig } from "../config/config"; import { ManagerHotkeySimulate } from "../hotkey/simulate"; import { executeHooks, executePluginHooks } from "../lib/hooks"; import { Manager } from "../manager"; import { PluginContext } from "../type"; import { ManagerWindow } from "../window"; import { ManagerPlugin } from "./index"; import { listModels, modelChat } from "./llm"; import { PluginLog } from "./log"; import { ManagerPluginPermission } from "./permission"; import { screenCapture } from "./screenCapture"; const getHeadHeight = (win: BrowserWindow) => { if (win === AppRuntime.mainWindow) { return WindowConfig.minHeight; } else { return WindowConfig.detachWindowTitleHeight; } }; export const ManagerPluginEvent = { pluginEvents: {} as { [event: string]: PluginContext[]; }, firePluginEvent: async (event: PluginEvent, data: any) => { if (event in ManagerPluginEvent.pluginEvents) { for (const context of ManagerPluginEvent.pluginEvents[event]) { await executePluginHooks( context as BrowserView, "PluginEvent", { event, data }, ); } } }, registerPluginEvent: async (context: PluginContext, data: any) => { // console.log('registerPluginEvent', context._plugin) const { event } = data; if (!(event in ManagerPluginEvent.pluginEvents)) { ManagerPluginEvent.pluginEvents[event] = []; } if (!ManagerPluginPermission.check(context._plugin, "event", event)) { return; } ManagerPluginEvent.pluginEvents[event].push(context); for (const e in ManagerPluginEvent.pluginEvents) { ManagerPluginEvent.pluginEvents[e] = ManagerPluginEvent.pluginEvents[e].filter((c) => { return !!(c as BrowserView).webContents; }); if (ManagerPluginEvent.pluginEvents[e].length === 0) { delete ManagerPluginEvent.pluginEvents[e]; } } }, unregisterPluginEvent: async (context: PluginContext, data: any) => { const { event } = data; if (!(event in ManagerPluginEvent.pluginEvents)) { return; } ManagerPluginEvent.pluginEvents[event] = ManagerPluginEvent.pluginEvents[event].filter((c) => c !== context); }, registerHotkey: async (context: PluginContext, data: any) => { if (!context._event) { context._event = {}; } if (!context._event["Hotkey"]) { context._event["Hotkey"] = []; } const { id, hotkeys } = data; context._event["Hotkey"].push({ id, hotkeys, }); }, unregisterHotkeyAll: async (context: PluginContext, data: any) => { if (!context._event || !context._event["HotKey"]) { return; } context._event["Hotkey"] = []; }, isMacOs: async (context: PluginContext, data: any) => { return isMac; }, isWindows: async (context: PluginContext, data: any) => { return isWin; }, isLinux: async (context: PluginContext, data: any) => { return isLinux; }, getPlatformArch: async (context: PluginContext, data: any) => { return platformArch(); }, isMainWindowShown: async (context: PluginContext, data: any) => { const win = AppRuntime.mainWindow; return win.isVisible(); }, isMainWindowFocused: async (context: PluginContext, data: any) => { const win = AppRuntime.mainWindow; return win.isFocused(); }, hideMainWindow: async (context: PluginContext, data: any) => { AppRuntime.mainWindow.hide(); }, showMainWindow: async (context: PluginContext, data: any) => { Manager.selectedContent = null; // Manager.selectedContent = await ManagerClipboard.getSelectedContent() Manager.activeWindow = await ManagerAutomation.getActiveWindow(); const { x: wx, y: wy } = AppPosition.get( "main", (screenX, screenY, screenWidth, screenHeight) => { // console.log('calculator', {screenX, screenY, screenWidth, screenHeight}); return { x: screenX + screenWidth / 2 - WindowConfig.mainWidth / 2, y: screenY + screenHeight / 8, }; }, ); const win = AppRuntime.mainWindow; win.setAlwaysOnTop(false); win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); win.focus(); win.setVisibleOnAllWorkspaces(false, { visibleOnFullScreen: true, }); win.setPosition(wx, wy); win.show(); }, isFastPanelWindowShown: async (context: PluginContext, data: any) => { const win = AppRuntime.fastPanelWindow; return win.isVisible() && win.isFocused(); }, showFastPanelWindow: async (context: PluginContext, data: any) => { Manager.selectedContent = await ManagerClipboard.getSelectedContent(); Manager.activeWindow = await ManagerAutomation.getActiveWindow(); const win = AppRuntime.fastPanelWindow; const { x, y } = AppPosition.getContextMenuPosition( WindowConfig.fastPanelWidth, WindowConfig.fastPanelHeight, ); win.setAlwaysOnTop(false); win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); win.focus(); win.setVisibleOnAllWorkspaces(false, { visibleOnFullScreen: true, }); win.setPosition(x, y); win.show(); }, hideFastPanelWindow: async (context: PluginContext, data: any) => { const win = AppRuntime.fastPanelWindow; win.hide(); }, showOpenDialog: async (context: PluginContext, data: any) => { return dialog.showOpenDialogSync(context._window, data); }, showSaveDialog: async (context: PluginContext, data: any) => { return dialog.showSaveDialogSync(context._window, data); }, setExpendHeight: async (context: PluginContext, data: any) => { const targetHeight = data as number; const win = context._window; win.setSize(win.getSize()[0], targetHeight); const screenPoint = screen.getCursorScreenPoint(); const display = screen.getDisplayNearestPoint(screenPoint); const position = win.getPosition()[1] + targetHeight > display.bounds.height ? targetHeight - getHeadHeight(win) : 0; // originWindow.webContents.executeJavaScript( // `window.setPosition && typeof window.setPosition === "function" && window.setPosition(${position})` // ); }, setSubInput: async (context: PluginContext, data: any) => { const win = context._window; const payload = { placeholder: data.placeholder, isFocus: data.isFocus, isVisible: data.isVisible, }; if (data.isFocus) { win.webContents.focus(); } await executeHooks(win, "SetSubInput", payload); }, removeSubInput: async (context: PluginContext, data: any) => { await executeHooks(context._window, "RemoveSubInput"); }, setSubInputValue: async (context: PluginContext, data: any) => { const { text } = data; await executeHooks(context._window, "SetSubInputValue", text); // this.sendSubInputChangeEvent({ data }); }, subInputBlur: async (context: PluginContext, data: any) => { (context as BrowserView).webContents.focus(); }, getPluginRoot: async (context: PluginContext, data: any) => { if (context._plugin.runtime && context._plugin.runtime.root) { return context._plugin.runtime.root; } return null; }, getPluginConfig: async (context: PluginContext, data: any) => { if (context._plugin) { return context._plugin; } return null; }, getPluginInfo: async (context: PluginContext, data: any) => { if (context._plugin) { return ManagerPlugin.getInfo(context._plugin); } return null; }, getPluginEnv: async (context: PluginContext, data: any) => { if (context._plugin) { if (context._plugin.env && context._plugin.env.env) { return context._plugin.env.env; } } return "prod"; }, getQuery: async ( context: PluginContext, data: any, ): Promise => { const { requestId } = data; return ( Manager.getSearchRequestQuery(requestId) || { keywords: "", currentFiles: [], currentImage: "", currentText: "", selectedContent: null, } ); }, getPath: async (context: PluginContext, data: any) => { return app.getPath(data.name); }, showToast: async (context: PluginContext, data: any) => { let { body, options } = data; options = Object.assign( { duration: 0, status: "success", }, options, ); AppsMain.toast(body, options); }, showNotification: async (context: PluginContext, data: any) => { let { body, clickActionName } = data; if (!Notification.isSupported()) { Log.error( "ManagerEvent.showNotification.Notification is not supported", ); return; } if ("string" != typeof body) { body = String(body); } const plugin = context._plugin; let icon = plugin.logo; if (icon && icon.startsWith("file://")) { icon = icon.substring(7); } const notify = new Notification({ title: plugin ? plugin.title : null, body, icon, }); notify.show(); }, showMessageBox: async (context: PluginContext, data: any) => { const { title, message, yes, no } = data; const buttons = []; if (yes) { buttons.push(yes); } if (no) { buttons.push(no); } const result = await dialog.showMessageBox({ type: "info", title: title || t("common.tip"), message: message, buttons: buttons, defaultId: 0, cancelId: 1, }); if (result.response === 0) { return true; } return false; }, copyImage: async (context: PluginContext, data: any) => { const { image } = data; let imageData; if (image.startsWith("data:image/")) { imageData = nativeImage.createFromDataURL(image); } else { imageData = nativeImage.createFromPath(image); } clipboard.writeImage(imageData); }, copyText: async (context: PluginContext, data: any) => { clipboard.writeText(String(data.text)); }, copyFile: async (context: PluginContext, data: any) => { let { file } = data; if (file) { if (!Array.isArray(file)) { file = [file]; } setClipboardFiles(file); return true; } return false; }, getClipboardText: async (context: PluginContext, data: any) => { return AppsMain.getClipboardText(); }, getClipboardImage: async (context: PluginContext, data: any) => { return AppsMain.getClipboardImage(); }, getClipboardFiles: async (context: PluginContext, data: any) => { return getClipboardFiles(); }, listClipboardItems: async (context: PluginContext, data: any) => { if ( !ManagerPluginPermission.checkPermit( context._plugin, "ClipboardManage", ) ) { throw new Error("Missing permission: ClipboardManage"); } let { option } = data; option = Object.assign( { limit: -1, }, option, ); return await ManagerClipboard.list(option.limit); }, deleteClipboardItem: async (context: PluginContext, data: any) => { if ( !ManagerPluginPermission.checkPermit( context._plugin, "ClipboardManage", ) ) { throw new Error("Missing permission: ClipboardManage"); } const { timestamp } = data; if (!timestamp) { throw new Error("Timestamp is required to delete clipboard item."); } return await ManagerClipboard.delete(timestamp); }, clearClipboardItems: async (context: PluginContext, data: any) => { if ( !ManagerPluginPermission.checkPermit( context._plugin, "ClipboardManage", ) ) { throw new Error("Missing permission: ClipboardManage"); } return await ManagerClipboard.clear(); }, shellBeep: async (context: PluginContext, data: any) => { shell.beep(); }, getFileIcon: async (context: PluginContext, data: any) => { const nativeImage = await app.getFileIcon(data.path, { size: "normal", }); return nativeImage.toDataURL(); }, shellShowItemInFolder: async (context: PluginContext, data: any) => { shell.showItemInFolder(data.path); }, simulateKeyboardTap: async (context: PluginContext, data: any) => { const { key, modifiers } = data; // 'ctrl' | 'shift' | 'command' | 'option' | 'alt' const modifiersNumber = modifiers.map((m) => { switch (m) { case "ctrl": return ManagerHotkeySimulate.toCode("Ctrl"); case "shift": return ManagerHotkeySimulate.toCode("Shift"); case "command": return ManagerHotkeySimulate.toCode("Meta"); case "option": case "alt": return ManagerHotkeySimulate.toCode("Alt"); } }); ManagerHotkeySimulate.keyTap( ManagerHotkeySimulate.toCode(key), modifiersNumber, ); }, simulateTypeString: async (context: PluginContext, data: any) => { const { text } = data; await ManagerAutomation.typeString(text); }, simulateMouseToggle: async (context: PluginContext, data: any) => { const { type, button } = data; await ManagerAutomation.mouseToggle(type, button); }, simulateMouseMove: async (context: PluginContext, data: any) => { const { x, y } = data; await ManagerAutomation.moveMouse(x, y); }, simulateMouseClick: async (context: PluginContext, data: any) => { const { button, double } = data; await ManagerAutomation.mouseClick(button, double); }, screenCapture: async (context: PluginContext, data: any) => { screenCapture((image: string) => { if (context["_screenCaptureCallback"]) { context["_screenCaptureCallback"]({ image }); } else { executePluginHooks(context as BrowserView, "ScreenCapture", { image: image, }); } }); }, getNativeId: async (context: PluginContext, data: any) => { return [platformName(), EncodeUtil.md5(platformUUID())].join("_"); }, getAppVersion: async (context: PluginContext, data: any) => { return AppConfig.version; }, outPlugin: async (context: PluginContext, data: any) => { const option: any = {}; if (context && context._window) { option.window = context._window; } await ManagerWindow.close(context._plugin, option); }, isDarkColors: async (context: PluginContext, data: any) => { return await AppsMain.shouldDarkMode(); }, showUserLogin: async (context: PluginContext, data: any) => { await PageUser.open({ parent: context._window, }); }, getUser: async ( context: PluginContext, data: any, ): Promise<{ isLogin: boolean; avatar: string; nickname: string; vipFlag: string; deviceCode: string; openId: string; } | null> => { const info = await User.get(); const user = info.user; const result = { isLogin: !!(user && user.id), avatar: user.avatar || "", nickname: user.name || "", vipFlag: info.data?.vip?.flag || "", deviceCode: user.deviceCode, openId: "", }; if (result.isLogin) { const res = await UserApi.post<{ openId: string; }>( "client/getUser", { pluginName: context._plugin.name, }, { throwException: false, }, ); if (res.code === 0) { if (res.data) { result.openId = res.data.openId; } } } return result; }, redirect: async (context: PluginContext, data: any) => { let { keywordsOrAction, query } = data; query = Object.assign( { keywords: "", currentFiles: [], currentImage: "", currentText: "", }, query, ); const action = await Manager.searchOneAction(keywordsOrAction, query); // console.log("redirect", {keywordsOrAction, query, action}); if (!action) { ManagerPluginEvent.showToast(context, { body: t("plugin.actionNotFound"), }); return; } await Manager.openAction(action); }, getActions: async (context: PluginContext, data: any) => { let { names } = data; names = names || []; const customActions = await ManagerConfig.getCustomAction(); const plugin = context._plugin; if (!(plugin.name in customActions)) { return []; } return customActions[plugin.name] .filter((m) => { if (names.length > 0) { return names.includes(m.name); } return true; }) .map((m) => { return m; }); }, setAction: async (context: PluginContext, data: any) => { const { action } = data; const plugin = context._plugin; await ManagerConfig.addCustomAction(plugin, action); }, removeAction: async (context: PluginContext, data: any) => { const { name } = data; const plugin = context._plugin; await ManagerConfig.removeCustomAction(plugin, name); }, callPage: async (context: PluginContext, data_: any) => { let { type, data, option } = data_; option = Object.assign( { waitReadyTimeout: 10 * 1000, timeout: 60 * 1000, showWindow: true, autoClose: true, }, option, ) as CallPageOption; const plugin = context._plugin; return new Promise((resolve, reject) => { ManagerWindow.open(plugin, null, { type: "callPage", callPage: { type, data, option, onResult(result) { if (result.code === 0) { resolve(result.data); } else { reject(new Error(result.msg || "Error")); } }, }, }); }); }, setRemoteWebRuntime: async (context: PluginContext, data: any) => { const { info } = data; const plugin = context._plugin; plugin.runtime.remoteWeb = { userAgent: info.userAgent || "", urlMap: info.urlMap || {}, types: info.types || [], blocks: info.blocks || [], domains: info.domains || [], }; }, llmListModels: async (context: PluginContext, data: any) => { return listModels(); }, llmChat: async (context: PluginContext, data: any) => { const { callInfo } = data; try { return modelChat( callInfo.providerId, callInfo.modelId, callInfo.message, ); } catch (e) { return { code: -1, msg: `Request failed: ${e instanceof Error ? e.message : String(e)}`, }; } }, logInfo: async (context: PluginContext, data: any) => { const { label, logData } = data; PluginLog.info(context._plugin.name, label, logData); }, logError: async (context: PluginContext, data: any) => { const { label, logData } = data; PluginLog.error(context._plugin.name, label, logData); }, logPath: async (context: PluginContext, data: any) => { return Log.appPath(PluginLog.name(context._plugin.name)); }, logShow: async (context: PluginContext, data: any) => { const p = Log.appPath(PluginLog.name(context._plugin.name)); Page.open("log", { log: p, }); }, addLaunch: async (context: PluginContext, data: any) => { const { keyword, name, hotkey } = data; if (!keyword || !name || !hotkey) { throw new Error("Keyword, name and hotkey are required."); } const hotkeyConvert = { key: hotkey.key, // Alt Option altKey: false, // Ctrl Control ctrlKey: false, // Command Win metaKey: false, // Shift shiftKey: false, times: 1, }; const modifiers = hotkey.modifiers || []; // "Control" | "Option" | "Command" | "Ctrl" | "Alt" | "Win" | "Meta" | "Shift" if (modifiers.includes("Control") || modifiers.includes("Ctrl")) { hotkeyConvert.ctrlKey = true; } if (modifiers.includes("Option") || modifiers.includes("Alt")) { hotkeyConvert.altKey = true; } if ( modifiers.includes("Command") || modifiers.includes("Win") || modifiers.includes("Meta") ) { hotkeyConvert.metaKey = true; } if (modifiers.includes("Shift")) { hotkeyConvert.shiftKey = true; } const records = await ManagerConfig.listLaunch(); const exists = records.find((m) => { return ( m.type === "plugin" && m.pluginName === context._plugin.name && m.keyword === keyword ); }); if (exists) { throw new Error(`Launch with keyword "${keyword}" already exists.`); } else { records.push({ type: "plugin", pluginName: context._plugin.name, keyword, name, hotkey: hotkeyConvert, }); } await ManagerConfig.updateLaunch(records); }, removeLaunch: async (context: PluginContext, data: any) => { const { keyword } = data; const records = await ManagerConfig.listLaunch(); const index = records.findIndex((m) => { return ( m.type === "plugin" && m.pluginName === context._plugin.name && m.keyword === keyword ); }); if (index >= 0) { records.splice(index, 1); await ManagerConfig.updateLaunch(records); } else { throw new Error(`Launch with keyword "${keyword}" not found.`); } }, activateLatestWindow: async (context: PluginContext, data: any) => { await ManagerAutomation.activateLatestWindow(); }, getUserAccessToken: async (context: PluginContext, data: any) => { const res = await UserApi.post<{ token: string; expireAt: number; }>("client/getUserAccessToken", { pluginName: context._plugin.name, }); return { token: res.data.token, expireAt: res.data.expireAt, }; }, listGoods: async (context: PluginContext, data: any) => { const { query } = data; const res = await UserApi.post<{ total: number; records: { id: string; title: string; cover: string; priceType: "fixed" | "dynamic"; fixedPrice: string; description: string; }[]; }>("client/listGoods", { pluginName: context._plugin.name, query, }); return res.data.records; }, openGoodsPayment: async (context: PluginContext, data: any) => { const { options } = data as { options: { goodsId: string; price?: string; outOrderId?: string; outParam?: string; }; }; const payResult = { paySuccess: false, }; return new Promise((resolve, reject) => { let watchUrl = null as any; let controller = null as any; PagePayment.open({ parent: context._window, onRefresh: async () => { // console.log('onRefresh') const res = await UserApi.post<{ payUrl: string; watchUrl: string; payExpireSeconds: number; body: string; }>("client/createGoodsOrder", { pluginName: context._plugin.name, ...options, }); watchUrl = res.data.watchUrl; return { payUrl: res.data.payUrl, watchUrl: res.data.watchUrl, payExpireSeconds: res.data.payExpireSeconds, body: res.data.body, }; }, onWatch: async () => { // console.log('onWatch') let status = "Error" as | "WaitPay" | "Scanned" | "Payed" | "Expired" | "Error"; const res = await UserApi.post<{ status: "unknown" | "WaitPay" | "Payed"; scanStatus: null | "Scanned"; }>(watchUrl, {}); if (res.data.status === "WaitPay") { if (res.data.scanStatus === "Scanned") { status = "Scanned"; } else { status = "WaitPay"; } } else if (res.data.status === "Payed") { status = "Payed"; } else if (res.data.status === "unknown") { status = "Error"; } if ("Payed" === status) { payResult.paySuccess = true; setTimeout(() => { controller.close(); }, 3000); } // console.log('watch', status, res) return { status, }; }, onClose: async () => { resolve(payResult); }, }).then((c) => { controller = c; }); }); }, queryGoodsOrders: async (context: PluginContext, data: any) => { const { options } = data as { options: { goodsId?: string; page?: number; pageSize?: number; }; }; const res = await UserApi.post<{ page: number; total: number; records: { id: string; goodsId: string; status: "Paid" | "Unpaid"; }[]; }>("client/queryGoodsOrders", { pluginName: context._plugin.name, ...options, }); return { page: res.data.page, total: res.data.total, records: res.data.records, }; }, apiPost: async (context: PluginContext, data: any) => { if (!ManagerPluginPermission.check(context._plugin, "basic", "Api")) { return; } const { url, body, option } = data; const res = await UserApi.post(url, body || {}, { throwException: false, }); return res; }, // file fileExists: async (context: PluginContext, data: any): Promise => { if (!ManagerPluginPermission.check(context._plugin, "basic", "File")) { return false; } const { path } = data; return await Files.exists(path, { isDataPath: false, }); }, fileRead: async (context: PluginContext, data: any) => { if (!ManagerPluginPermission.check(context._plugin, "basic", "File")) { return; } const { path, format } = data; if ("buffer" === format) { return await Files.readBuffer(path); } if ("base64" === format) { const content = await Files.readBuffer(path); return content.toString("base64"); } return await Files.read(path, { isDataPath: false, encoding: "utf-8", }); }, fileWrite: async (context: PluginContext, data: any) => { if (!ManagerPluginPermission.check(context._plugin, "basic", "File")) { return; } let { path, data: content, option } = data; option = Object.assign( { isBase64: false, }, option, ); if (typeof content === "string") { if (option.isBase64) { if (content.startsWith("data:")) { content = content.split(",")[1]; } content = Buffer.from(content, "base64"); return await Files.writeBuffer(path, content); } return await Files.write(path, content); } return await Files.writeBuffer(path, content); }, fileRemove: async (context: PluginContext, data: any): Promise => { if (!ManagerPluginPermission.check(context._plugin, "basic", "File")) { return; } const { path } = data; return await Files.deletes(path, { isDataPath: false, }); }, fileExt: async (context: PluginContext, data: any) => { const { path } = data; const ext = Files.ext(path); return ext ? ext : ""; }, fileWriteTemp: async (context: PluginContext, data_: any) => { if (!ManagerPluginPermission.check(context._plugin, "basic", "File")) { return; } let { ext, data, option } = data_; option = Object.assign( { isBase64: false, }, option, ); const tempPath = await Files.temp(ext); if (option?.isBase64) { // remove prefix data:image/svg+xml;base64, if ((data as string).startsWith("data:")) { data = (data as string).split(",")[1]; } data = Buffer.from(data as string, "base64"); } fs.writeFileSync(tempPath, data as Uint8Array); return tempPath; }, // db dbPut: async (context: PluginContext, data: any) => { return await KVDBMain.put(context._plugin.name, data.doc); }, dbGet: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.get(context._plugin.name, data.id); }, dbRemove: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.remove(context._plugin.name, data.doc); }, dbBulkDocs: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.bulkDocs(context._plugin.name, data.docs); }, dbAllDocs: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.allDocs(context._plugin.name, data.key); }, dbPostAttachment: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.postAttachment( context._plugin.name, data.docId, data.attachment, data.type, ); }, dbGetAttachment: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.getAttachment(context._plugin.name, data.docId); }, dbGetAttachmentType: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); return await KVDBMain.getAttachmentType( context._plugin.name, data.docId, ); }, // dbStorage dbStorageSetItem: async (context: PluginContext, data: any) => { // const plugin = ManagerWindow.getPluginByWindow(win); const plugin = context._plugin; const { key, value } = data; const id = `${CommonConfig.dbPluginStorageIdPrefix}/${key}`; const doc = { _id: id, data: value, _rev: undefined }; const result = await KVDBMain.get(plugin.name, id); if (result) { doc._rev = result._rev; } const res = await KVDBMain.put(plugin.name, doc); if ((res as DBError).error) throw new Error((res as DBError).message); }, dbStorageGetItem: async (context: PluginContext, data: any) => { const plugin = context._plugin; const { key } = data; const id = `${CommonConfig.dbPluginStorageIdPrefix}/${key}`; const result = await KVDBMain.get(plugin.name, id); return result ? result.data : null; }, dbStorageRemoveItem: async (context: PluginContext, data: any) => { const plugin = context._plugin; const { key } = data; const id = `${CommonConfig.dbPluginStorageIdPrefix}/${key}`; const result = await KVDBMain.get(plugin.name, id); if (!result) return; await KVDBMain.remove(plugin.name, result); }, detachSetTitle: async (context: PluginContext, data: any) => { const { title } = data; await executeHooks(context._window, "DetachSet", { title, }); }, detachSetOperates: async (context: PluginContext, data: any) => { const { operates } = data; await executeHooks(context._window, "DetachSet", { operates, }); }, detachSetPosition: async (context: PluginContext, data: any) => { const { position } = data; const win = context._window; const winSize = win.getSize(); const { x, y } = AppsMain.calcPositionInCurrentDisplay( position, winSize[0], winSize[1], ); win.setPosition(x, y); }, detachSetAlwaysOnTop: async (context: PluginContext, data: any) => { const { alwaysOnTop } = data; const win = context._window; win.setAlwaysOnTop(alwaysOnTop); await executeHooks(context._window, "DetachSet", { alwaysOnTop, }); }, detachSetSize: async (context: PluginContext, data: any) => { const { width, height } = data; const win = context._window; win.setSize(width, height); }, }; ================================================ FILE: electron/mapi/manager/plugin/http.ts ================================================ import express from "express"; import Apps from "../../app"; import { Log } from "../../log/main"; import { ManagerPlugin } from "./index"; import { PluginRecord } from "../../../../src/types/Manager"; import { serveMcpRPC, serveMcpSSE } from "./httpMCP"; async function servePluginStatic(req, res) { const paths: string[] = req.params.path; if (!paths || !paths.length) { res.status(404).send("Plugin Static Server : Not Found"); return; } // console.log('servePluginStatic', paths); const pluginName = paths.shift(); const pluginFile = paths.join("/"); const plugin: PluginRecord = await ManagerPlugin.get(pluginName); if (!plugin) { res.status(404).send("Plugin Static Server : Not Found"); return; } if (!plugin.setting?.httpEntry) { res.status(404).send( "Plugin Static Server : Plugin HTTP Entry Not Enabled", ); return; } express.static(plugin.runtime.root)( Object.assign(req, { url: `/${pluginFile}` }), res, (err) => { if (err) { res.status(500).send("Plugin Static Server : " + err.message); } else { res.status(404).send("Plugin Static Server : Not Found"); } }, ); } export const PluginHttp = { app: null, port: 61000, ip: "127.0.0.1", async init() { PluginHttp.app = express(); PluginHttp.app.use(express.json()); PluginHttp.app.all("/plugin/*path", servePluginStatic); PluginHttp.app.post("/mcp", serveMcpRPC); PluginHttp.app.get("/mcp", serveMcpSSE); PluginHttp.port = await Apps.availablePort(PluginHttp.port); return new Promise((resolve, reject) => { PluginHttp.app.listen(PluginHttp.port, PluginHttp.ip, () => { Log.info("PluginHttp.Listen", { port: PluginHttp.port }); }); }); }, async getMcpServer() { if (!PluginHttp.app) { await PluginHttp.init(); } return `http://${PluginHttp.ip}:${PluginHttp.port}/mcp`; }, async url(pluginName: string, filePath: string): Promise { if (!PluginHttp.app) { return new Promise((resolve) => { setTimeout(() => { resolve(PluginHttp.url(pluginName, filePath)); }, 100); }); } if (!pluginName || !filePath) { throw new Error("Plugin name and file path are required"); } return `http://${PluginHttp.ip}:${PluginHttp.port}/plugin/${pluginName}/${filePath}`; }, }; ================================================ FILE: electron/mapi/manager/plugin/httpMCP.ts ================================================ import { ManagerPlugin } from "./index"; import { Log } from "../../log/main"; import { MCPToolsRecord } from "../../../../src/types/Manager"; import { ManagerBackend } from "../backend"; import { PluginLog } from "./log"; const clients = new Map(); export async function serveMcpSSE(req, res) { res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.write(`event: ready\n`); res.write(`data: {}\n\n`); clients.set(res, true); Log.info("MCPServer.SSE.Connected", { total: clients.size }); req.on("close", () => { clients.delete(res); Log.info("MCPServer.SSE.Disconnected", { total: clients.size }); }); } export async function serveMcpRPC(req, res) { const body = req.body; if (!body || typeof body !== "object") { res.json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse Error" }, }); Log.error("MCPServer", { error: "Invalid JSON" }); return; } // check jsonrpc 2.0 if (body.jsonrpc !== "2.0") { res.json({ jsonrpc: "2.0", id: body.id || null, error: { code: -32600, message: "Invalid Request, only JSON-RPC 2.0 supported", }, }); Log.error("MCPServer", { error: "Invalid JSON-RPC version", body }); return; } const method = body.method; if (!method || typeof method !== "string") { res.json({ jsonrpc: "2.0", id: body.id || null, error: { code: -32600, message: "Invalid Request, method required", }, }); Log.error("MCPServer", { error: "Method not specified", body }); return; } const id = body.id; if (!id || (typeof id !== "string" && typeof id !== "number")) { if ( ![ "initialize", "notifications/initialized", "notifications/cancelled", ].includes(method) ) { res.json({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request, id required", }, }); Log.error("MCPServer", { error: "ID not specified", body }); return; } } if (!PluginHttpMCP[method]) { res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" }, }); Log.error("MCPServer", { error: "Method not found", method, body }); return; } const params = body.params || {}; try { const result = await PluginHttpMCP[method](params); const json = { jsonrpc: "2.0", id, result }; Log.info("MCPServer.call", { method, params, json }); res.json(json); } catch (e) { Log.error("MCPServer.call", { method, params, error: e + "", }); res.json({ jsonrpc: "2.0", id, error: { code: -32000, message: e + "" }, }); } } export const PluginHttpMCP = { initialize: async (params: Record) => { return { protocolVersion: "2024-11-05", capabilities: { tools: { listChanged: false, }, }, serverInfo: { name: "FocusAny MCP Server", version: "1.0.0", }, }; }, "notifications/initialized": async (params: Record) => { return {}; }, "notifications/cancelled": async (params: Record) => { return {}; }, "tools/list": async (params: Record) => { const tools: MCPToolsRecord[] = []; const plugins = await ManagerPlugin.list(); for (const plugin of plugins) { if (plugin.mcp) { if (plugin.mcp.tools && Array.isArray(plugin.mcp.tools)) { for (const tool of plugin.mcp.tools) { tools.push({ ...tool, name: `${plugin.name}-${tool.name}`, }); } } } } return { tools, }; }, "tools/call": async (params: Record) => { const { name, arguments: args } = params; const pcs = name.split("-"); if (pcs.length < 2) { throw new Error("Invalid tool name"); } const pluginName = pcs.shift()!; const toolName = pcs.join("-"); const plugin = await ManagerPlugin.get(pluginName); if (!plugin) { throw new Error("Plugin not found"); } const result: any = await ManagerBackend.run( plugin, "mcpTool", toolName, args || {}, { rejectIfError: true }, ); if (!result) { PluginLog.error( plugin.name, `MCP.Tool.NoResult`, { toolName, args, }, true, ); throw new Error("No result from tool"); } if (result.content && Array.isArray(result.content)) { result.content = result.content.map((item) => { if (item.type === "image") { // remove prefix data:image/png;base64,iVBORw if (item.data && item.data.startsWith("data:image")) { const idx = item.data.indexOf("base64,"); if (idx > 0) { item.data = item.data.substring(idx + 7); } } } return item; }); } return result; }, }; setTimeout(async () => { // PluginHttpMCP['tools/call']({ // name: 'BasicExample.basic-example-weather', // arguments: {city: 'Beijing'}, // }) // const result = await PluginHttpMCP['tools/call']({ // name: 'FabricEditor.fileToPng', // arguments: {path: '/Users/mz/Downloads/NewFile.FabricEditor.fad'}, // }) // Log.info('MCPServer.Test', {result}); }, 5000); ================================================ FILE: electron/mapi/manager/plugin/index.ts ================================================ import { ActionMatch, ActionMatchKey, ActionMatchRegex, ActionMatchText, ActionMatchTypeEnum, ActionRecord, ActionTypeEnum, PluginEnv, PluginRecord, PluginType, } from "../../../../src/types/Manager"; import { Files } from "../../file/main"; import { preloadDefault, preloadPluginDefault, rendererDistPath, rendererIsUrl, } from "../../../lib/env-main"; import { join } from "node:path"; import { KVDBMain } from "../../kvdb/main"; import { CommonConfig } from "../../../config/common"; import { MemoryCacheUtil, StrUtil, UIUtil, VersionUtil, } from "../../../lib/util"; import { MiscMain } from "../../misc/main"; import { platformName } from "../../../lib/env"; import { AppConfig } from "../../../../src/config"; import { WindowConfig } from "../../../config/window"; import { AppsMain } from "../../app/main"; import { ManagerConfig } from "../config/config"; import { ManagerBackend } from "../backend"; import { session } from "electron"; import { PluginHttp } from "./http"; type PluginInfo = { type: PluginType; name: string; version: string; root: string; config: PluginRecord; }; export const ManagerPlugin = { installingMap: {} as { [name: string]: { version: string; startTime: number; }; }, async clearCache() { MemoryCacheUtil.forget("Plugins"); MemoryCacheUtil.forget("PluginActions"); }, async getInfo(plugin: PluginRecord) { // nodeIntegration let nodeIntegration = false; if (plugin.type === PluginType.SYSTEM) { nodeIntegration = true; } else if (plugin.setting && plugin.setting.nodeIntegration) { nodeIntegration = true; } // preloadBase let preloadBase = preloadPluginDefault; if (plugin.setting && plugin.setting.preloadBase) { preloadBase = plugin.setting.preloadBase; if (preloadBase === "") { preloadBase = preloadDefault; } } // preload let preload = plugin.preload || null; if (preload) { if (preload === "") { preload = preloadDefault; } else { preload = join(plugin.runtime?.root, preload); } } if (preload && preloadBase === preload) { preload = null; } // main && mainView let main = plugin.main || null; if (main && plugin.setting?.httpEntry) { main = await PluginHttp.url(plugin.name, main); } if (!main) { main = rendererDistPath("static/pluginEmpty.html"); } let mainView = plugin.mainView || null; if (!mainView) { mainView = main; } if (mainView && plugin.setting?.httpEntry) { mainView = await PluginHttp.url(plugin.name, mainView); } if (plugin.runtime?.root) { if (!rendererIsUrl(main)) { main = join(plugin.runtime?.root, main); } } else if (main.includes("")) { main = main.replace("/", ""); main = rendererDistPath(main); } if (plugin.runtime?.root) { if (!rendererIsUrl(mainView)) { mainView = join(plugin.runtime?.root, mainView); } } else if (mainView.includes("")) { mainView = mainView.replace("/", ""); mainView = rendererDistPath(mainView); } if (!rendererIsUrl(mainView)) { mainView = `file://${mainView}`; } // auto detach let autoDetach = false; if (plugin.setting && plugin.setting.autoDetach) { autoDetach = true; } if ( !autoDetach && plugin.runtime.config && plugin.runtime.config.autoDetach ) { autoDetach = true; } // width & height let width = WindowConfig.pluginWidth; let height = WindowConfig.pluginHeight; if (plugin.setting) { const display = AppsMain.getCurrentScreenDisplay(); if (plugin.setting.width) { width = UIUtil.sizeToPx( plugin.setting.width + "", display.workArea.width, ); autoDetach = true; } if (plugin.setting.height) { height = UIUtil.sizeToPx( plugin.setting.height + "", display.workArea.height, ); autoDetach = true; } } // singleton let singleton = true; if (plugin.setting && "singleton" in plugin.setting) { singleton = false; } // zoom let zoom = 100; if (plugin.setting && plugin.setting.zoom) { zoom = plugin.setting.zoom; } if (plugin.runtime.config && plugin.runtime.config.zoom) { zoom = plugin.runtime.config.zoom; } return { nodeIntegration, preloadBase, preload, main, mainView, width, height, autoDetach, singleton, zoom, }; }, normalAction(action: ActionRecord, plugin: PluginRecord) { const matches: ActionMatch[] = []; for (let m of action.matches) { if (typeof m === "string") { m = { type: ActionMatchTypeEnum.TEXT, text: m, } as any; } if (!m.name) { switch (m.type) { case ActionMatchTypeEnum.TEXT: m.name = (m as ActionMatchText).text; break; case ActionMatchTypeEnum.KEY: m.name = (m as ActionMatchKey).key; break; case ActionMatchTypeEnum.REGEX: m.name = (m as ActionMatchRegex).regex; break; case ActionMatchTypeEnum.FILE: case ActionMatchTypeEnum.IMAGE: case ActionMatchTypeEnum.WINDOW: m.name = StrUtil.hashCode(JSON.stringify(m)); break; } } matches.push(m); } if (!("trackHistory" in action)) { action.trackHistory = true; } const normalAction = { fullName: `${plugin.name}/${action.name}`, pluginName: plugin.name, name: action.name, title: action.title || plugin.title, icon: action.icon || plugin.logo, trackHistory: action.trackHistory, type: action.type || ActionTypeEnum.WEB, pluginType: plugin.type, matches: matches, data: action.data || {}, platform: action.platform || ["win", "osx", "linux"], } as ActionRecord; if (plugin.runtime.root) { if (normalAction.icon && !normalAction.icon.startsWith("file://")) { normalAction.icon = `file://${plugin.runtime.root}/${normalAction.icon}`; } } return normalAction; }, async initIfNeed( plugin: PluginRecord, option: { type: PluginType; root?: string; configJson?: any; }, ): Promise { option = Object.assign( { type: null, }, option, ); if (!option.type) { throw "PluginTypeError"; } // console.log('ManagerPlugin.init', plugin.name, !plugin.runtime) if (plugin.runtime) { return plugin; } plugin.platform = plugin.platform || ["win", "osx", "linux"]; plugin.versionRequire = plugin.versionRequire || "*"; plugin.editionRequire = plugin.editionRequire || ["open", "pro"]; plugin.logo = plugin.logo || null; plugin.main = plugin.main || null; plugin.mainView = plugin.mainView || plugin.main; plugin.preload = plugin.preload || null; plugin.author = plugin.author || null; plugin.homepage = plugin.homepage || null; if (!plugin.mcp) { plugin.mcp = {}; } if (!plugin.mcp.tools) { plugin.mcp.tools = []; } plugin.setting = Object.assign( { remoteWebCacheEnable: false, httpEntry: false, moreMenu: [], }, plugin.setting || {}, ); plugin.development = Object.assign( { showDevTools: false, showCodeDevTools: false, keepCodeDevTools: false, }, plugin.development, ); plugin.type = option.type; plugin.env = PluginEnv.PROD; plugin.runtime = { root: option.root, config: await ManagerConfig.getPluginConfig(plugin.name), }; if (plugin.runtime.root) { if (plugin.logo && !plugin.logo.startsWith("file://")) { plugin.logo = `file://${plugin.runtime.root}/${plugin.logo}`; } } for (let aIndex = 0; aIndex < plugin.actions.length; aIndex++) { const a = this.normalAction(plugin.actions[aIndex], plugin); if (!a.platform.includes(platformName())) { continue; } plugin.actions[aIndex] = a; } const configJson = option.configJson || null; if (configJson) { if (configJson["development"]) { plugin.env = PluginEnv.DEV; if (configJson["development"].env) { plugin.env = configJson["development"].env as any; } if (PluginEnv.DEV === plugin.env) { if (configJson["development"].main) { plugin.main = configJson["development"].main; } if (configJson["development"].mainView) { plugin.mainView = configJson["development"].mainView; } } } } return plugin; }, async configCheck(config: any) { if (!config) { throw `PluginFormatError:-1`; } if (!config.name || !config.version) { throw `PluginFormatError:-2`; } const existsP = await this.get(config.name); if (existsP) { throw `PluginAlreadyExists : ${config.name}`; } if (!config.platform) { config.platform = ["win", "osx", "linux"]; } if (!config.platform.includes(platformName())) { throw `PluginNotSupportPlatform : ${config.name}`; } if (!config.versionRequire) { config.versionRequire = "*"; } if (!VersionUtil.match(AppConfig.version, config.versionRequire)) { throw `PluginVersionNotMatch:-2:${config.name}`; } if (!config.editionRequire) { config.editionRequire = ["open", "pro"]; } if (!config.editionRequire.includes("open")) { throw `PluginEditionNotMatch:-1:${config.name}`; } }, async parsePackage(file: string, option?: {}) { option = Object.assign({}, option); if (!file.endsWith(".zip")) { throw `PluginFormatError:-3`; } let config = null; try { config = await MiscMain.getZipFileContent(file, "config.json"); } catch (e) { throw `PluginFormatError:-4`; } if (!config) { throw `PluginFormatError:-5`; } try { config = JSON.parse(config as string); } catch (e) { throw `PluginFormatError:-6`; } if (!config) { throw `PluginFormatError:-7`; } if (!config.name || !config.version) { throw `PluginFormatError:-8`; } const target = await Files.fullPath(`plugin/${config.name}`); return { name: config.name, version: config.version, target, }; }, async installFromFileOrDir(fileOrPath: string, type?: PluginType) { let guessType = type || PluginType.DIR; if ( !(await Files.isDirectory(fileOrPath, { isDataPath: false, })) ) { if (fileOrPath.endsWith("config.json")) { fileOrPath = fileOrPath.replace(/[\/\\]config.json$/, ""); } else { guessType = PluginType.ZIP; const { name, version, target } = await this.parsePackage(fileOrPath); const plugin = await ManagerPlugin.get(name); if ( await Files.exists(target, { isDataPath: false, }) ) { if (!plugin) { await Files.deletes(target, { isDataPath: false, }); } } try { await MiscMain.unzip(fileOrPath, target); fileOrPath = target; } catch (e) { throw "PluginInstallError"; } } } return await this.install(fileOrPath, type || guessType); }, async install(root: string, type: PluginType) { const p = await this._readPluginInfo(root); if (!p) { throw `PluginNotValid : ${root}`; } const existsP = await this.get(p.name); if (existsP) { throw `PluginAlreadyExists : ${p.name}`; } const plugin = await this.initIfNeed(p, { type, root, configJson: p, }); if (!plugin.platform.includes(platformName())) { throw `PluginNotSupportPlatform : ${plugin.name}`; } if (!VersionUtil.match(AppConfig.version, plugin.versionRequire)) { throw `PluginVersionNotMatch:-1:${plugin.name}`; } if (!plugin.editionRequire.includes("open")) { throw `PluginEditionNotMatch:-2:${plugin.name}`; } const runtime = plugin.runtime; delete plugin.runtime; const info: PluginInfo = { type, version: plugin.version, name: plugin.name, root, config: plugin, }; await KVDBMain.putForce(CommonConfig.dbSystem, { _id: `${CommonConfig.dbPluginIdPrefix}/${info.name}`, ...info, }); await this.clearCache(); setTimeout(async () => { plugin.runtime = runtime; await ManagerBackend.run(plugin, "hook", "installed", {}); }, 1000); }, async refreshInstall(name: string) { const doc = await KVDBMain.get( CommonConfig.dbSystem, `${CommonConfig.dbPluginIdPrefix}/${name}`, ); if (!doc) { throw `PluginNotExists : ${name}`; } const pluginInfo: PluginInfo = doc as any; const root = pluginInfo.root; const p = await this._readPluginInfo(root); // console.log('refreshInstall', JSON.stringify({name, root, p}, null, 2)) if (!p) { throw `PluginNotValid : ${root}`; } const plugin = await this.initIfNeed(p, { type: pluginInfo.type, root, configJson: p, }); const runtime = plugin.runtime; delete plugin.runtime; const info: PluginInfo = { type: pluginInfo.type, version: plugin.version, name: plugin.name, root, config: plugin, }; await KVDBMain.putForce(CommonConfig.dbSystem, { _id: `${CommonConfig.dbPluginIdPrefix}/${info.name}`, ...info, }); await this.clearCache(); setTimeout(async () => { plugin.runtime = runtime; await ManagerBackend.run(plugin, "hook", "installed", {}); }, 1000); }, async uninstall(name: string) { const plugin = await this.get(name); if (!plugin) { throw `PluginNotExists:-1:${name}`; } const pi = await KVDBMain.get( CommonConfig.dbSystem, `${CommonConfig.dbPluginIdPrefix}/${name}`, ); if (!pi) { throw `PluginNotExists:-2:${name}`; } const info: PluginInfo = pi as any; if (!info.name || !info.version || !info.type || !info.config) { throw `PluginNotExists:-3:${name}`; } await ManagerBackend.run(plugin, "hook", "beforeUninstall", {}); if (info.type === PluginType.STORE || info.type === PluginType.ZIP) { if (info.root) { await Files.deletes(info.root, { isDataPath: false, }); } } await KVDBMain.remove(CommonConfig.dbSystem, pi); await ManagerConfig.clearCustomAction(name); await this.clearCache(); await this.clearViewSession(plugin); }, async getPluginInstalledVersion(name: string) { const plugin = await this.get(name); if (!plugin) { return null; } return plugin.version; }, async isPluginInstalling(name: string) {}, async list(): Promise { const plugins = await MemoryCacheUtil.remember( "Plugins", async () => { // await this.install(`${process.cwd()}/plugin-examples/plugin-example`, 'system') let plugins: PluginRecord[] = []; const pluginInfos = await KVDBMain.allDocs( CommonConfig.dbSystem, `${CommonConfig.dbPluginIdPrefix}/`, ); for (const pi of pluginInfos) { const info: PluginInfo = pi as any; if ( !info.name || !info.version || !info.type || !info.config ) { await KVDBMain.remove(CommonConfig.dbSystem, pi); continue; } let configJson = null; if (info.type === PluginType.DIR) { configJson = await this._readPluginInfo(info.root); info.config = configJson; if (!info.config) { // 本地插件可能已经被删除 await KVDBMain.remove(CommonConfig.dbSystem, pi); continue; } } plugins.push( await this.initIfNeed(info.config, { type: info.type, root: info.root, configJson, }), ); } // console.log('plugins', JSON.stringify(plugins)) return plugins; }, ); // 有开发选项并且是开发环境的插件,每次都重新读取 config for (let pIndex = 0; pIndex < plugins.length; pIndex++) { const p = plugins[pIndex]; if ( p.type === PluginType.DIR && p.env === "dev" && p.runtime.root ) { const configJson = await this._readPluginInfo(p.runtime.root); plugins[pIndex] = await this.initIfNeed(p, { type: p.type, root: p.runtime.root, configJson, }); } } return plugins; }, async get(name: string) { for (const p of await this.list()) { if (p.name === name) { return p; } } return null; }, async _readPluginInfo(root: string) { root = root.replace(/[\\/]+$/, ""); const configPath = root + "/config.json"; const config = await Files.read(configPath, { isDataPath: false, }); if (!config) { return null; } try { let configJson = JSON.parse(config); if (!configJson) { return null; } return configJson; } catch (e) {} return null; }, async listAction() { return await MemoryCacheUtil.remember( "PluginActions", async () => { let actions: ActionRecord[] = []; const plugins = await this.list(); for (const p of plugins) { actions = actions.concat(p.actions); } return actions; }, ); }, async getViewSession(plugin: PluginRecord, name: string = null) { if (name) { return session.fromPartition("<" + plugin.name + `:${name}>`); } return session.fromPartition("<" + plugin.name + ">"); }, async clearViewSession(plugin: PluginRecord) { const viewSession = await this.getViewSession(plugin); if (viewSession) { await viewSession.clearStorageData(); } }, isDevelopmentCheck( plugin: PluginRecord, key: keyof NonNullable, ) { if (!plugin.development || plugin.development.env !== PluginEnv.DEV) { return false; } return !!plugin.development[key]; }, }; ================================================ FILE: electron/mapi/manager/plugin/llm.ts ================================================ // @ts-ignore import { Model, Provider } from "../../../../src/module/Model/types"; // @ts-ignore import { getProviderLogo, getProviderTitle, SystemProviders, } from "../../../../src/module/Model/providers"; // @ts-ignore import { SystemModels } from "../../../../src/module/Model/models"; import StorageMain from "../../storage/main"; import User from "../../user/main"; import { AppConfig } from "../../../../src/config"; import { ModelProvider } from "../../../../src/module/Model/provider/provider"; const listProviders = async (): Promise => { const results: Provider[] = []; for (const providerId in SystemProviders) { const provider = SystemProviders[providerId]; results.push({ id: providerId, type: "openai", title: getProviderTitle(providerId), logo: getProviderLogo(providerId), isSystem: true, apiUrl: provider.api.url, websites: { official: provider.websites?.official, docs: provider.websites?.docs, models: provider.websites?.models, }, data: { apiKey: "", apiHost: "", models: (SystemModels[providerId] || []).map((m) => { return { id: m.id, provider: providerId, name: m.name, group: m.group, types: ["text"], enabled: false, } as any; }), enabled: false, }, }); } const storageData = await StorageMain.read("models", []); let buildInProviderData: any = null; if (storageData) { if (storageData.userProviders) { storageData.userProviders.forEach((provider) => { results.unshift({ id: provider.id, type: provider.type, title: provider.title, logo: null, isSystem: false, apiUrl: "", websites: { official: "", docs: "", models: "", }, data: { apiKey: "", apiHost: "", models: [], enabled: false, }, }); }); } if (storageData.providerData) { buildInProviderData = storageData.providerData["buildIn"] || null; for (const providerId in storageData.providerData) { const provider = results.find((p) => p.id === providerId); if (provider) { provider.data.apiKey = storageData.providerData[providerId].apiKey || ""; provider.data.apiHost = storageData.providerData[providerId].apiHost; (storageData.providerData[providerId].models || []).forEach( (model) => { const existingModel = provider.data.models.find( (m) => m.id === model.id, ); if (existingModel) { existingModel.name = model.name; existingModel.group = model.group; existingModel.types = model.types; existingModel.enabled = model.enabled || false; } else { provider.data.models.push({ id: model.id, provider: providerId, name: model.name, group: model.group, types: ["text"], enabled: model.enabled || false, editable: true, }); } }, ); provider.data.enabled = storageData.providerData[providerId].enabled || false; } } } } const user = await User.get(); if (user.data && user.data.lmApi && user.data.lmApi.models) { const lmApi = user.data.lmApi; const models: Model[] = []; for (const m of lmApi.models) { models.push({ id: m, provider: "buildIn", name: m, group: "Default", types: ["text"], enabled: true, editable: false, }); } let enabled = true; if (buildInProviderData && "enabled" in buildInProviderData) { enabled = buildInProviderData.enabled; } results.unshift({ id: "buildIn", type: "openai", title: getProviderTitle("buildIn"), logo: getProviderLogo("buildIn"), isSystem: true, apiUrl: lmApi.apiUrl, websites: { official: AppConfig.website, docs: AppConfig.website, models: AppConfig.website, }, data: { apiKey: lmApi.apiKey, apiHost: "", models: models, enabled: enabled, }, }); } return results; }; export const listModels = async () => { const providers = await listProviders(); const results: { providerId: string; providerLogo: string; providerTitle: string; modelId: string; modelName: string; }[] = []; for (const provider of providers) { if (!provider.data || !provider.data.enabled || !provider.data.models) { continue; } for (const model of provider.data.models) { if (model.enabled) { results.push({ providerId: provider.id, providerLogo: provider.logo || "", providerTitle: provider.title, modelId: model.id, modelName: model.name, }); } } } return results; }; export const modelChat = async ( providerId: string, modelId: string, message: string, ): Promise<{ code: number; msg: string; data?: { message: string; }; }> => { const providers = await listProviders(); const provider = providers.find((p) => p.id === providerId); if (!provider) { throw new Error(`Provider not found: ${providerId}`); } const model = provider.data.models.find((m) => m.id === modelId); if (!model || !model.enabled) { throw new Error(`Model not found or not enabled: ${modelId}`); } const res = await ModelProvider.chat(message, { type: provider.type, modelId: model.id, apiUrl: provider.apiUrl, apiHost: provider.data.apiHost, apiKey: provider.data.apiKey, }); if (res.code) { return { code: -1, msg: res.msg, }; } return { code: 0, msg: "ok", data: { message: res.data.content, }, }; }; ================================================ FILE: electron/mapi/manager/plugin/log.ts ================================================ import { t } from "../../../config/lang"; import { AppsMain } from "../../app/main"; import { Log } from "../../log/main"; export const PluginLog = { name: (pluginName: string) => { return `Plugin_${pluginName}`; }, info: (pluginName: string, label: string, data: any) => { const name = PluginLog.name(pluginName); Log.appInfo(name, label, data); }, error: ( pluginName: string, label: string, data: any, toast: boolean = false, ) => { const name = PluginLog.name(pluginName); Log.appError(name, label, data); if (toast) { AppsMain.toast( t("plugin.errorLog", { name: pluginName, error: label, }), ).then(); } }, }; ================================================ FILE: electron/mapi/manager/plugin/permission.ts ================================================ import { PluginPermissionType, PluginRecord, } from "../../../../src/types/Manager"; import { t } from "../../../config/lang"; import { AppsMain } from "../../app/main"; export const ManagerPluginPermission = { checkPermit( plugin: PluginRecord, permission: PluginPermissionType, ): boolean { if ( plugin.permissions && plugin.permissions.length > 0 && plugin.permissions.includes(permission) ) { return true; } AppsMain.toast(t("plugin.noPermission", { permission }), { status: "error", }); return false; }, /** * check if the plugin has permission for a specific type and typeData * @param plugin * @param type basic | event * @param typeData */ check( plugin: PluginRecord, type: "basic" | "event", typeData: string, ): boolean { // console.log('ManagerPluginPermission.check', JSON.stringify(plugin, null, 2)) if ("basic" === type) { return this.checkPermit(plugin, typeData as PluginPermissionType); } else if ("event" === type) { if (typeData === "ClipboardChange") { return this.checkPermit(plugin, "ClipboardManage"); } else if (["UserChange"].includes(typeData)) { return true; } } AppsMain.toast( t("plugin.noPermission", { permission: `${type}.${typeData}` }), { status: "error" }, ); return false; }, }; ================================================ FILE: electron/mapi/manager/plugin/screenCapture.ts ================================================ import { exec, execFile } from "child_process"; import { clipboard, Notification } from "electron"; import { t } from "../../../config/lang"; import { extraResolve, isMac, isWin } from "../../../lib/env"; const forWindows = (cb: (image: string) => void) => { const screenCaptureUrl = extraResolve("win/ScreenCapture.exe"); const screen_window = execFile(screenCaptureUrl); screen_window.on("exit", (code) => { if (code) { const image = clipboard.readImage(); cb && cb(image.isEmpty() ? "" : image.toDataURL()); } }); }; const forMac = (cb: (image: string) => void) => { exec("screencapture -i -r -c", () => { const image = clipboard.readImage(); cb && cb(image.isEmpty() ? "" : image.toDataURL()); }); }; const forLinux = (cb: (image: string) => void) => { const notify = new Notification({ title: t("system.screenshot"), body: t("plugin.screenshotHint"), }); notify.show(); }; export const screenCapture = (cb: (image: string) => void) => { clipboard.writeText(""); if (isMac) { forMac(cb); } else if (isWin) { forWindows(cb); } else { forLinux(cb); } }; ================================================ FILE: electron/mapi/manager/plugin/screenRecord.ts ================================================ import { spawn } from "child_process"; import { BrowserWindow, desktopCapturer, dialog, screen } from "electron"; import { t } from "../../../config/lang"; let isRecording = false; let ffmpegProcess: any = null; let recordingWindow: BrowserWindow | null = null; // let tray: Tray = (global as any).tray; const screenRecord = async (): Promise => { if (isRecording) { stopRecording(); return; } // 选择保存路径 const result = await dialog.showSaveDialog({ title: t("plugin.selectSavePath"), defaultPath: "screen_record.mp4", filters: [{ name: "MP4", extensions: ["mp4"] }], }); if (result.canceled) return; const savePath = result.filePath; // 获取屏幕源 const sources = await desktopCapturer.getSources({ types: ["screen"] }); if (sources.length === 0) return; // 选择屏幕,假设第一个 const source = sources[0]; const displays = screen.getAllDisplays(); const display = displays.find((d) => d.id === Number(source.display_id)) || displays[0]; // 选择区域 const bounds = await selectArea(display.bounds); if (!bounds) return; // 开始录制 startRecording(savePath, source.id, bounds, display); }; const selectArea = async (screenBounds: any): Promise => { return new Promise((resolve) => { const win = new BrowserWindow({ x: screenBounds.x, y: screenBounds.y, width: screenBounds.width, height: screenBounds.height, frame: false, transparent: true, alwaysOnTop: true, webPreferences: { nodeIntegration: true, contextIsolation: false, }, }); win.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(` `)}`, ); win.webContents.on("console-message", (event, level, message) => { if (message.startsWith("SELECT:")) { const bounds = JSON.parse(message.substring(7)); win.close(); resolve(bounds); } }); win.on("closed", () => resolve(null)); }); }; const startRecording = ( savePath: string, sourceId: string, bounds: any, display: any, ) => { isRecording = true; // 创建录制指示器窗口 recordingWindow = new BrowserWindow({ x: display.bounds.x + bounds.x, y: display.bounds.y + bounds.y, width: bounds.width, height: bounds.height, frame: false, transparent: true, alwaysOnTop: true, webPreferences: { nodeIntegration: false, contextIsolation: true, }, }); recordingWindow.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(` `)}`, ); // 改变tray // if (tray) { // // 假设停止图标路径 // const stopIconPath = path.join(__dirname, '../../../public/iconfont/stop.png'); // 需要实际图标 // tray.setImage(stopIconPath); // tray.setToolTip('点击停止录制'); // tray.removeAllListeners('click'); // tray.on('click', () => stopRecording()); // } // ffmpeg命令 const platform = process.platform; let args: string[]; if (platform === "darwin") { const screenIndex = parseInt(sourceId.split(":")[1]) + 1; args = [ "-f", "avfoundation", "-i", `${screenIndex}`, "-vf", `crop=${bounds.width}:${bounds.height}:${bounds.x}:${bounds.y}`, "-c:v", "libx264", "-preset", "fast", "-crf", "22", "-c:a", "aac", savePath, ]; } else if (platform === "win32") { args = [ "-f", "gdigrab", "-i", "desktop", "-vf", `crop=${bounds.width}:${bounds.height}:${bounds.x}:${bounds.y}`, "-c:v", "libx264", "-preset", "fast", "-crf", "22", savePath, ]; } else { args = [ "-f", "x11grab", "-i", ":0.0", "-vf", `crop=${bounds.width}:${bounds.height}:${bounds.x}:${bounds.y}`, "-c:v", "libx264", "-preset", "fast", "-crf", "22", savePath, ]; } ffmpegProcess = spawn("ffmpeg", args); ffmpegProcess.on("close", () => { stopRecording(); }); }; const stopRecording = () => { if (!isRecording) return; isRecording = false; if (ffmpegProcess) { ffmpegProcess.kill("SIGINT"); ffmpegProcess = null; } // 关闭录制指示器窗口 if (recordingWindow) { recordingWindow.close(); recordingWindow = null; } // 恢复tray // if (tray) { // const defaultIconPath = path.join(__dirname, '../../../public/static/tray/icon.png'); // 假设默认图标路径 // tray.setImage(defaultIconPath); // tray.setToolTip('FocusAny'); // tray.removeAllListeners('click'); // // 恢复原始点击事件 // tray.on('click', () => { // // 假设显示主界面 // }); // } }; export { screenRecord }; ================================================ FILE: electron/mapi/manager/plugin/sdk.ts ================================================ import { BrowserWindow, screen, shell } from "electron"; import os from "os"; import path from "path"; import { PluginRecord } from "../../../../src/types/Manager"; import { t } from "../../../config/lang"; import { EncodeUtil, FileUtil, StrUtil, TimeUtil } from "../../../lib/util"; import { PluginContext } from "../type"; import { ManagerPluginEvent } from "./event"; import { PluginLog } from "./log"; export const PluginSdkCreate = (plugin: PluginRecord) => { const context = { _window: null, _plugin: plugin, } as PluginContext; const sdk = { async isMacOs() { return os.type() === "Darwin"; }, async isWindows() { return os.type() === "Windows_NT"; }, async isLinux() { return os.type() === "Linux"; }, async getPlatformArch() { return ManagerPluginEvent.getPlatformArch(context, {}); }, async isMainWindowShown() { return ManagerPluginEvent.isMainWindowShown(context, {}); }, async hideMainWindow() { return ManagerPluginEvent.hideMainWindow(context, {}); }, async showMainWindow() { return ManagerPluginEvent.showMainWindow(context, {}); }, async isFastPanelWindowShown() { return ManagerPluginEvent.isFastPanelWindowShown(context, {}); }, async showFastPanelWindow() { return ManagerPluginEvent.showFastPanelWindow(context, {}); }, async hideFastPanelWindow() { return ManagerPluginEvent.hideFastPanelWindow(context, {}); }, async showOpenDialog() { return ManagerPluginEvent.showOpenDialog(context, {}); }, async showSaveDialog() { return ManagerPluginEvent.showSaveDialog(context, {}); }, async getPluginRoot() { return plugin.runtime?.root; }, async getPluginConfig() { return ManagerPluginEvent.getPluginConfig(context, {}); }, async getPluginInfo() { return ManagerPluginEvent.getPluginInfo(context, {}); }, async getPluginEnv() { return ManagerPluginEvent.getPluginEnv(context, {}); }, async getPath( name: | "home" | "appData" | "userData" | "temp" | "exe" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "logs", ) { return ManagerPluginEvent.getPath(context, { name }); }, async showToast( body: string, options?: { duration?: number; status?: "info" | "success" | "error"; }, ) { ManagerPluginEvent.showToast(context, { body, options }).then(); }, async showNotification(body: string, clickActionName: string) { return ManagerPluginEvent.showNotification(context, { body, clickActionName, }); }, async showMessageBox( message: string, options: { title?: string; yes?: string; no?: string; }, ) { return ManagerPluginEvent.showMessageBox(context, { message, ...options, }); }, async copyImage(img: string) { return ManagerPluginEvent.copyImage(context, { img }); }, async copyText(text: string) { return ManagerPluginEvent.copyText(context, { text }); }, async copyFile(file: string) { return ManagerPluginEvent.copyFile(context, { file }); }, async getClipboardText() { return ManagerPluginEvent.getClipboardText(context, {}); }, async getClipboardImage() { return ManagerPluginEvent.getClipboardImage(context, {}); }, async getClipboardFiles(): Promise< { name: string; pathname: string; isDirectory: boolean; size: number; lastModified: number; }[] > { return (await ManagerPluginEvent.getClipboardFiles( context, {}, )) as any; }, async listClipboardItems(option?: { limit?: number }): Promise< { type: "file" | "image" | "text"; timestamp: number; files?: FileItem[]; image?: string; text?: string; }[] > { return ManagerPluginEvent.listClipboardItems(context, option || {}); }, async deleteClipboardItem(timestamp: number): Promise { return ManagerPluginEvent.deleteClipboardItem(context, { timestamp, }); }, async clearClipboardItems(): Promise { return ManagerPluginEvent.clearClipboardItems(context, {}); }, async shellOpenExternal(url: string) { await shell.openExternal(url); }, async shellOpenPath(path: string) { await shell.openPath(path).then(); }, async shellShowItemInFolder(path: string) { await ManagerPluginEvent.shellShowItemInFolder(context, { path }); }, async shellBeep() { return ManagerPluginEvent.shellBeep(context, {}); }, async getFileIcon(path: string) { return ManagerPluginEvent.getFileIcon(context, { path }); }, simulate: { async keyboardTap( key: string, modifiers: ("ctrl" | "shift" | "command" | "option" | "alt")[], ) { await ManagerPluginEvent.simulateKeyboardTap(context, { key, modifiers, }); }, async typeString(text: string) { await ManagerPluginEvent.simulateTypeString(context, { text }); }, async mouseToggle( type: "down" | "up", button: "left" | "right" | "middle", ) { await ManagerPluginEvent.simulateMouseToggle(context, { type, button, }); }, async mouseMove(x: number, y: number) { await ManagerPluginEvent.simulateMouseMove(context, { x, y }); }, async mouseClick( button: "left" | "right" | "middle", double?: boolean, ) { await ManagerPluginEvent.simulateMouseClick(context, { button, double, }); }, }, async getCursorScreenPoint() { return screen.getCursorScreenPoint(); }, async getDisplayNearestPoint(point: { x: number; y: number }) { return screen.getDisplayNearestPoint(point); }, // sendTo async createBrowserWindow(url: string, options: any, callback: any) { const pluginRoot = await this.getPluginRoot(); url = path.join(pluginRoot, url); let preloadPath = null; if (options.webPreferences && options.webPreferences.preload) { preloadPath = path.join( pluginRoot, options.webPreferences.preload, ); } if (url.startsWith("http://") || url.startsWith("https://")) { // do nothing } else { url = `file://${url}`; } options = options || {}; let win = new BrowserWindow({ useContentSize: true, resizable: true, title: options.title || t("plugin.newWindow"), show: true, backgroundColor: "#fff", ...options, webPreferences: { webSecurity: false, backgroundThrottling: false, contextIsolation: false, webviewTag: true, nodeIntegration: true, spellcheck: false, partition: null, ...(options.webPreferences || {}), preload: preloadPath, }, }); win.loadURL(url); win.on("closed", () => { win = undefined; }); win.once("ready-to-show", () => { win.show(); }); win.webContents.on("dom-ready", () => { callback && callback(); }); return win; }, async screenCapture(cb: Function) { context["_screenCaptureCallback"] = (data: { image: string }) => { cb && cb(data.image); }; return ManagerPluginEvent.screenCapture(context, { cb }); }, getNativeId() { return ManagerPluginEvent.getNativeId(context, {}); }, getAppVersion() { return ManagerPluginEvent.getAppVersion(context, {}); }, async isDarkColors() { return ManagerPluginEvent.isDarkColors(context, {}); }, async redirect(keywordsOrAction: string | string[], payload: any) { return ManagerPluginEvent.redirect(context, { keywordsOrAction, payload, }); }, async getActions(names?: string[]) { return ManagerPluginEvent.getActions(context, { names }); }, async setAction(action: string) { return ManagerPluginEvent.setAction(context, { action }); }, async removeAction(name: string) { return ManagerPluginEvent.removeAction(context, { name }); }, async sendBackendEvent( event: string, data?: any, option?: { timeout: number; }, ): Promise { throw new Error("Only can be called in plugin web"); }, registerCallPage( type: string, callback: ( resolve: (data: any) => void, reject: (error: string) => void, data: any, ) => void, option?: { timeout?: number; }, ) { throw new Error("Only can be called in plugin web"); }, callPage( type: string, data?: any, option?: CallPageOption, ): Promise { return ManagerPluginEvent.callPage(context, { type, data, option }); }, setRemoteWebRuntime(info: { userAgent: string; urlMap: Record; types: string[]; domains: string[]; blocks: string[]; }): Promise { throw new Error("Only can be called in plugin web"); }, async llmListModels(): Promise< { providerId: string; providerLogo: string; providerTitle: string; modelId: string; modelName: string; }[] > { return ManagerPluginEvent.llmListModels(context, {}); }, async llmChat(callInfo: { providerId: string; modelId: string; message: string; }): Promise<{ code: number; msg: string; data?: { message: string; }; }> { return ManagerPluginEvent.llmChat(context, { callInfo }); }, logInfo(label: string, data?: any): void { ManagerPluginEvent.logInfo(context, { label, logData: data }); }, logError(label: string, data?: any): void { ManagerPluginEvent.logError(context, { label, logData: data }); }, async logPath(): Promise { return ManagerPluginEvent.logPath(context, {}); }, logShow(): void { ManagerPluginEvent.logShow(context, {}); }, async addLaunch( keyword: string, name: string, hotkey: HotkeyType, ): Promise { return ManagerPluginEvent.addLaunch(context, { keyword, name, hotkey, }); }, async removeLaunch(keyword: string): Promise { return ManagerPluginEvent.removeLaunch(context, { keyword }); }, async activateLatestWindow(): Promise { return ManagerPluginEvent.activateLatestWindow(context, {}); }, async getUser(): Promise<{ avatar: string; nickname: string; vipFlag: string; deviceCode: string; } | null> { return ManagerPluginEvent.getUser(context, {}); }, async getUserAccessToken(): Promise<{ token: string; expireAt: number; }> { return ManagerPluginEvent.getUserAccessToken(context, {}); }, file: { async exists(path: string): Promise { return ManagerPluginEvent.fileExists(context, { path }); }, async read(path: string): Promise { return ManagerPluginEvent.fileRead(context, { path }); }, async write(path: string, data: string): Promise { return ManagerPluginEvent.fileWrite(context, { path, data }); }, async remove(path: string): Promise { return ManagerPluginEvent.fileRemove(context, { path }); }, async ext(path: string): Promise { return ManagerPluginEvent.fileExt(context, { path }); }, async writeTemp( ext: string, data: string | Uint8Array, option?: { isBase64?: boolean; }, ): Promise { return ManagerPluginEvent.fileWriteTemp(context, { ext, data, option, }); }, }, db: { async put(doc: { _id: string; data: any; _rev?: string }) { return ManagerPluginEvent.dbPut(context, { doc }); }, async get(id: string) { return ManagerPluginEvent.dbGet(context, { id }); }, async remove( doc: | { _id: string; } | string, ) { return ManagerPluginEvent.dbRemove(context, { doc }); }, async bulkDocs( docs: { _id: string; data: any; _rev?: string; }[], ) { return ManagerPluginEvent.dbBulkDocs(context, { docs }); }, async allDocs(key: string | string[]) { return ManagerPluginEvent.dbAllDocs(context, { key }); }, async postAttachment( docId: string, attachment: Buffer | Uint8Array, type: string, ) { return ManagerPluginEvent.dbPostAttachment(context, { docId, attachment, type, }); }, async getAttachment(docId: string) { return ManagerPluginEvent.dbGetAttachment(context, { docId }); }, async getAttachmentType(docId: string) { return ManagerPluginEvent.dbGetAttachmentType(context, { docId, }); }, }, dbStorage: { async setItem(key: string, value: any) { return ManagerPluginEvent.dbStorageSetItem(context, { key, value, }); }, async getItem(key: string) { return ManagerPluginEvent.dbStorageGetItem(context, { key }); }, async removeItem(key: string) { return ManagerPluginEvent.dbStorageRemoveItem(context, { key }); }, }, util: { randomString(length: number) { return StrUtil.randomString(length); }, bufferToBase64(buffer: Buffer) { return FileUtil.bufferToBase64(buffer); }, datetimeString() { return TimeUtil.datetimeString(); }, base64Encode(data: any) { return EncodeUtil.base64Encode(data); }, base64Decode(data: string) { return EncodeUtil.base64Decode(data); }, md5(data: string) { return EncodeUtil.md5(data); }, }, }; const createDeepProxy = (target: any, cache = new WeakMap()) => { if (typeof target !== "object" || target === null) { return target; } if (cache.has(target)) { return cache.get(target); } const proxy = new Proxy(target, { get(obj, prop) { const value = Reflect.get(obj, prop); if (typeof value === "function") { return async function (...args: any[]) { try { return await Promise.resolve( value.apply(obj, args), ); } catch (error) { PluginLog.error( plugin.name, `SDK-${prop.toString()}`, { error: error + "", }, ); } }; } if (typeof value === "object" && value !== null) { return createDeepProxy(value, cache); } return value; }, }); cache.set(target, proxy); return proxy; }; return createDeepProxy(sdk); }; ================================================ FILE: electron/mapi/manager/render.ts ================================================ import { ipcRenderer } from "electron"; import { ActionRecord, ConfigRecord, PluginRecord, } from "../../../src/types/Manager"; const getConfig = async () => { return ipcRenderer.invoke("manager:getConfig"); }; const setConfig = async (config: ConfigRecord) => { return ipcRenderer.invoke("manager:setConfig", config); }; const getMcpServer = async () => { return ipcRenderer.invoke("manager:getMcpServer"); }; const getMcpInfo = async () => { return ipcRenderer.invoke("manager:getMcpInfo"); }; const isShown = async () => { return ipcRenderer.invoke("manager:isShown"); }; const show = async () => { return ipcRenderer.invoke("manager:show"); }; const hide = async () => { return ipcRenderer.invoke("manager:hide"); }; const getClipboardContent = () => { return ipcRenderer.invoke("manager:getClipboardContent"); }; const getClipboardChangeTime = () => { return ipcRenderer.invoke("manager:getClipboardChangeTime"); }; const getSelectedContent = async () => { return ipcRenderer.invoke("manager:getSelectedContent"); }; const listPlugin = async (option?: {}) => { return ipcRenderer.invoke("manager:listPlugin", option); }; const installPlugin = async (fileOrPath: string, option?: {}) => { return ipcRenderer.invoke("manager:installPlugin", fileOrPath, option); }; const refreshInstallPlugin = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke( "manager:refreshInstallPlugin", pluginName, option, ); }; const uninstallPlugin = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:uninstallPlugin", pluginName, option); }; const getPluginInstalledVersion = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke( "manager:getPluginInstalledVersion", pluginName, option, ); }; const listDisabledActionMatch = async (option?: {}) => { return ipcRenderer.invoke("manager:listDisabledActionMatch", option); }; const toggleDisabledActionMatch = async ( pluginName: string, actionName: string, matchName: string, option?: {}, ) => { return ipcRenderer.invoke( "manager:toggleDisabledActionMatch", pluginName, actionName, matchName, option, ); }; const listPinAction = async (option?: {}) => { return ipcRenderer.invoke("manager:listPinAction", option); }; const togglePinAction = async ( pluginName: string, actionName: string, option?: {}, ) => { return ipcRenderer.invoke( "manager:togglePinAction", pluginName, actionName, option, ); }; const showLog = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:showLog", pluginName, option); }; const clearCache = async (option?: {}) => { return ipcRenderer.invoke("manager:clearCache", option); }; const hotKeyWatch = async (option?: {}) => { return ipcRenderer.invoke("manager:hotKeyWatch", option); }; const hotKeyUnwatch = async (option?: {}) => { return ipcRenderer.invoke("manager:hotKeyUnwatch", option); }; const searchFastPanelAction = async ( query: { currentFiles: any[]; currentImage: string; }, option?: {}, ) => { return ipcRenderer.invoke("manager:searchFastPanelAction", query, option); }; const searchAction = async ( query: { keywords: string; currentFiles: any[]; currentImage: string; }, option?: {}, ) => { return ipcRenderer.invoke("manager:searchAction", query, option); }; const listDetachWindowActions = async (option?: {}) => { return ipcRenderer.invoke("manager:listDetachWindowActions", option); }; const subInputChange = (keywords: string, option?: {}) => { return ipcRenderer.invoke("manager:subInputChange", keywords, option); }; const openPlugin = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:openPlugin", pluginName, option); }; const openAction = async (action: ActionRecord) => { return ipcRenderer.invoke("manager:openAction", action); }; const openActionCode = async (id: string) => { return ipcRenderer.invoke("manager:openActionCode", id); }; const searchActionCode = async (keywords: string) => { return ipcRenderer.invoke("manager:searchActionCode", keywords); }; const openActionWindow = async ( type: "open" | "close", action: ActionRecord, ) => { return ipcRenderer.invoke("manager:openActionWindow", type, action); }; const closeMainPlugin = async (option?: {}) => { return ipcRenderer.invoke("manager:closeMainPlugin", option); }; const openMainPluginDevTools = async (option?: {}) => { return ipcRenderer.invoke("manager:openMainPluginDevTools", option); }; const openMainPluginLog = async (option?: {}) => { return ipcRenderer.invoke("manager:openMainPluginLog", option); }; const detachPlugin = async (option?: {}) => { return ipcRenderer.invoke("manager:detachPlugin", option); }; const toggleDetachPluginAlwaysOnTop = async ( alwaysOnTop: boolean, option?: {}, ) => { return ipcRenderer.invoke( "manager:toggleDetachPluginAlwaysOnTop", alwaysOnTop, option, ); }; const setDetachPluginZoom = async (zoom: number, option?: {}) => { return ipcRenderer.invoke("manager:setDetachPluginZoom", zoom, option); }; const firePluginMoreMenuClick = async (name: string, option?: {}) => { return ipcRenderer.invoke("manager:firePluginMoreMenuClick", name, option); }; const fireDetachOperateClick = async (name: string, option?: {}) => { return ipcRenderer.invoke("manager:fireDetachOperateClick", name, option); }; const closeDetachPlugin = async (option?: {}) => { return ipcRenderer.invoke("manager:closeDetachPlugin"); }; const openDetachPluginDevTools = async (option?: {}) => { return ipcRenderer.invoke("manager:openDetachPluginDevTools", option); }; const openDetachPluginLog = async (option?: {}) => { return ipcRenderer.invoke("manager:openDetachPluginLog", option); }; const setPluginAutoDetach = async (autoDetach: boolean, option?: {}) => { return ipcRenderer.invoke( "manager:setPluginAutoDetach", autoDetach, option, ); }; const getPluginConfig = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:getPluginConfig", pluginName, option); }; const listFilePluginRecords = async (option?: {}) => { return ipcRenderer.invoke("manager:listFilePluginRecords", option); }; const updateFilePluginRecords = async ( records: PluginRecord[], option?: {}, ) => { return ipcRenderer.invoke( "manager:updateFilePluginRecords", records, option, ); }; const listLaunchRecords = async (option?: {}) => { return ipcRenderer.invoke("manager:listLaunchRecords", option); }; const updateLaunchRecords = async (records: PluginRecord[], option?: {}) => { return ipcRenderer.invoke("manager:updateLaunchRecords", records, option); }; const storeInstall = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:storeInstall", pluginName, option); }; const storePublish = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:storePublish", pluginName, option); }; const storePublishInfo = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke("manager:storePublishInfo", pluginName, option); }; const storeInstallingInfo = async (pluginName: string, option?: {}) => { return ipcRenderer.invoke( "manager:storeInstallingInfo", pluginName, option, ); }; const clipboardList = async (option?: {}) => { return ipcRenderer.invoke("manager:clipboardList", option); }; const clipboardClear = async (option?: {}) => { return ipcRenderer.invoke("manager:clipboardClear", option); }; const clipboardDelete = async (timestamp: number, option?: {}) => { return ipcRenderer.invoke("manager:clipboardDelete", timestamp, option); }; const historyClear = async (option?: {}) => { return ipcRenderer.invoke("manager:historyClear", option); }; const historyDelete = async ( pluginName: string, actionName: string, option?: {}, ) => { return ipcRenderer.invoke( "manager:historyDelete", pluginName, actionName, option, ); }; export default { getConfig, setConfig, getMcpServer, getMcpInfo, isShown, show, hide, getClipboardContent, getClipboardChangeTime, getSelectedContent, listPlugin, installPlugin, refreshInstallPlugin, uninstallPlugin, getPluginInstalledVersion, listDisabledActionMatch, toggleDisabledActionMatch, listPinAction, togglePinAction, showLog, clearCache, hotKeyWatch, hotKeyUnwatch, searchFastPanelAction, searchAction, listDetachWindowActions, subInputChange, openPlugin, openAction, openActionCode, searchActionCode, openActionWindow, closeMainPlugin, openMainPluginDevTools, openMainPluginLog, detachPlugin, toggleDetachPluginAlwaysOnTop, setDetachPluginZoom, firePluginMoreMenuClick, fireDetachOperateClick, closeDetachPlugin, openDetachPluginDevTools, openDetachPluginLog, setPluginAutoDetach, getPluginConfig, listFilePluginRecords, updateFilePluginRecords, listLaunchRecords, updateLaunchRecords, storeInstall, storePublish, storePublishInfo, storeInstallingInfo, clipboardList, clipboardClear, clipboardDelete, historyClear, historyDelete, }; ================================================ FILE: electron/mapi/manager/storage/index.ts ================================================ export const ManagerStorage = { listPlugins() {}, }; ================================================ FILE: electron/mapi/manager/system/asset/icon.ts ================================================ import pluginSystem from "./plugin-system.svg"; import pluginStore from "./plugin-store.svg"; import pluginWorkflow from "./plugin-workflow.svg"; import pluginApp from "./plugin-app.svg"; import searchKeyword from "./search-keyword.svg"; import searchMatch from "./search-match.svg"; import searchWindow from "./search-window.svg"; import command from "./command.svg"; import database from "./database.svg"; import folder from "./folder.svg"; import model from "./model.svg"; import mcp from "./mcp.svg"; import screenshot from "./screenshot.svg"; import colorPicker from "./color-picker.svg"; import screenRecord from "./screen-record.svg"; import plugin from "./plugin.svg"; import thunder from "./thunder.svg"; import guide from "./guide.svg"; import user from "./user.svg"; import about from "./about.svg"; import apple from "./apple.svg"; import windows from "./windows.svg"; import linux from "./linux.svg"; import lock from "./lock.svg"; import ip from "./ip.svg"; export const SystemIcons = { pluginSystem, pluginStore, pluginWorkflow, pluginApp, searchKeyword, searchMatch, searchWindow, command, database, folder, model, mcp, screenshot, colorPicker, screenRecord, plugin, thunder, guide, user, about, apple, windows, linux, lock, ip, }; ================================================ FILE: electron/mapi/manager/system/index.ts ================================================ import { ActionRecord, PluginRecord, PluginType, } from "../../../../src/types/Manager"; import { SystemPlugin } from "./plugin/system"; import { SystemActionCode } from "./plugin/system/action"; import { StorePlugin } from "./plugin/store"; import { StoreActionCode } from "./plugin/store/action"; import { MemoryCacheUtil } from "../../../lib/util"; import { ManagerPlugin } from "../plugin"; import { getAppPlugin } from "./plugin/app"; import { getFilePlugin } from "./plugin/file"; const pluginActionCode = { system: SystemActionCode, store: StoreActionCode, }; const systemPlugin = new Set(["system", "store", "workflow", "app", "file"]); const pluginActionBackend = {}; export const ManagerSystem = { async clearCache() { for (const p of await this.list()) { delete p.runtime; } MemoryCacheUtil.forget("SystemActions"); }, match(name: string) { return systemPlugin.has(name); }, async list() { const plugins: (PluginRecord | any)[] = [ SystemPlugin, StorePlugin, getAppPlugin, getFilePlugin, ]; for (let i = 0; i < plugins.length; i++) { if (typeof plugins[i] === "function") { plugins[i] = await plugins[i](); } plugins[i] = await ManagerPlugin.initIfNeed(plugins[i], { type: PluginType.SYSTEM, root: null, }); } return plugins as PluginRecord[]; }, getActionCodeFunc(pluginName: string, name: string) { if (!pluginActionCode[pluginName]) { return null; } return pluginActionCode[pluginName][name] || null; }, getActionBackendFunc(pluginName: string, name: string) { if (!pluginActionBackend[pluginName]) { return null; } return pluginActionBackend[pluginName][name] || null; }, async listAction() { return await MemoryCacheUtil.remember( "SystemActions", async () => { let actions: ActionRecord[] = []; const plugins = await this.list(); for (const p of plugins) { actions = actions.concat(p.actions); } return actions; }, ); }, }; ================================================ FILE: electron/mapi/manager/system/plugin/app/linux/icon.ts ================================================ import path from "node:path"; import fs from "node:fs"; export const getIcon = async ( desktopInfo: Record, pathname: string, name: string, ) => { if (!desktopInfo.Icon) { return null; } const themes = ["hicolor"]; const sizes = ["scalable", "512x512", "256x256", "48x48", "32x32"]; const types = ["apps"]; const exts = [".png", ".svg"]; for (const theme of themes) { for (const size of sizes) { for (const type of types) { for (const ext of exts) { let iconPath = path.join( "/usr/share/icons", theme, size, type, desktopInfo.Icon + ext, ); if (fs.existsSync(iconPath)) { return "file://" + iconPath; } } } } } return null; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/linux/index.ts ================================================ import { listFiles } from "../util"; import path from "path"; import { ConfigLang } from "../../../../../../config/lang"; import { getIcon } from "./icon"; import { getAppTitle } from "./title"; import fs from "node:fs"; export const ManagerAppLinux = { list: async () => { return lists(); }, }; const appSet = new Set(); const lists = async () => { appSet.clear(); const files = await listFiles([ "/usr/share/applications", "/var/lib/snapd/desktop/applications", `${process.env.HOME}/.local/share/applications`, ]); const apps = []; const locale = ConfigLang.getLocale(); for (const f of files) { if (appSet.has(f.pathname)) { // console.log('appSet.has', f.pathname) continue; } const extname = path.extname(f.pathname); if (extname !== ".desktop") { continue; } const app = { name: f.name.replace(/\.(desktop)$/, ""), title: f.name, pathname: f.pathname, icon: null, command: null, }; const desktopInfo = await parseDesktopFile(app.pathname); app.icon = await getIcon(desktopInfo, app.pathname, app.name); app.title = await getAppTitle( desktopInfo, locale, app.pathname, app.name, ); if (!app.icon) { continue; } if (!desktopInfo.Exec) { continue; } let command = desktopInfo.Exec.replace(/ %[A-Za-z]/g, "") .replace(/"/g, "") .trim(); if (desktopInfo.Terminal === "true") { command = `gnome-terminal -x ${command}`; } app.command = command; appSet.add(app.pathname); apps.push(app); } return apps; }; const parseDesktopFile = async ( pathname: string, ): Promise> => { const content = fs.readFileSync(pathname, "utf-8"); const desktop = {}; for (const line of content.split("\n")) { if (line.startsWith("[")) { continue; } const [key, value] = line.split("="); if (!key || !value) { continue; } desktop[key] = value; } return desktop; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/linux/title.ts ================================================ const langDirMap = { "zh-CN": ["zh_CN"], }; export const getAppTitle = async ( desktopInfo: Record, locale: string, pathname: string, name: string, ) => { if (locale in langDirMap) { for (const k of langDirMap[locale]) { const infoKey = `Name[${k}]`; if (desktopInfo[infoKey]) { return desktopInfo[infoKey]; } } } if (desktopInfo.Name) { return desktopInfo.Name; } return name; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/mac/icon.ts ================================================ import path from "node:path"; import fs from "fs"; import { exec } from "child_process"; import { Files } from "../../../../../file/main"; import { AppEnv, waitAppEnvReady } from "../../../../../env"; const getIconTempDir = async () => { await waitAppEnvReady(); return path.join(AppEnv.dataRoot, "cache", "app-icons"); }; // console.log('iconTempDir', iconTempDir) const defaultIcon = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns"; const getIconFile = (appFileInput) => { return new Promise((resolve, reject) => { const plistPath = path.join(appFileInput, "Contents", "Info.plist"); Files.read(plistPath, { isDataPath: false, }) .then((plistContent) => { if (plistContent) { // parse CFBundleIconFile const mat = plistContent.match( /CFBundleIconFile<\/key>\s*(.*?)<\/string>/, ); if (mat) { const CFBundleIconFile = mat[1]; const iconFile = path.join( appFileInput, "Contents", "Resources", CFBundleIconFile, ); const iconFiles = [ iconFile, iconFile + ".icns", iconFile + ".tiff", ]; const existedIcon = iconFiles.find((iconFile) => { return fs.existsSync(iconFile); }); // console.log('manager.app.mac.app2png.getIconFile', existedIcon) resolve(existedIcon || defaultIcon); return; } } resolve(defaultIcon); }) .catch((e) => { console.log("manager.app.mac.app2png.getIconFile.error", e); resolve(defaultIcon); }); }); }; const tiffToPng = (iconFile, pngFileOutput) => { return new Promise((resolve, reject) => { exec( `sips -s format png '${iconFile}' --out '${pngFileOutput}' --resampleHeightWidth 64 64`, (error) => { error ? reject(error) : resolve(null); }, ); }); }; const app2png = (appFileInput, pngFileOutput) => { return getIconFile(appFileInput).then((iconFile) => { // console.log('manager.app.mac.app2png.app2png', iconFile, pngFileOutput) return tiffToPng(iconFile, pngFileOutput); }); }; export const getIcon = async (appPath: string, appName: string) => { try { const iconTempDir = await getIconTempDir(); const iconPathUrl = "file://" + path.join(iconTempDir, `${encodeURIComponent(appName)}.png`); const iconPath = path.join(iconTempDir, `${appName}.png`); if (await Files.exists(iconPath, { isDataPath: false })) { return iconPathUrl; } const iconNone = path.join(iconTempDir, `${appName}.none`); const iconNoneUrl = path.join(iconTempDir, `${appName}.none`); if (await Files.exists(iconNone, { isDataPath: false })) { return iconNoneUrl; } if (!(await Files.exists(iconTempDir, { isDataPath: false }))) { fs.mkdirSync(iconTempDir, { recursive: true }); } await app2png(appPath, iconPath); if (!(await Files.exists(iconPath, { isDataPath: false }))) { fs.writeFileSync(iconNone, ""); throw "IconGetError"; } return iconPathUrl; } catch (e) {} return `file://${defaultIcon}`; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/mac/index.ts ================================================ import { listFiles } from "../util"; import path from "node:path"; import { AppRecord } from "../type"; import { getIcon } from "./icon"; import { ConfigLang } from "../../../../../../config/lang"; import { getAppTitle } from "./title"; const appSet = new Set(); export const ManagerAppMac = { list: async () => { return lists(); }, }; const lists = async (): Promise => { appSet.clear(); let files = await listFiles([ "/Applications", "~/Applications", "/System/Applications", "/System/Library/PreferencePanes", ]); const apps = []; const locale = ConfigLang.getLocale(); for (const f of files) { if (appSet.has(f.pathname)) { // console.log('appSet.has', f.pathname) continue; } const extname = path.extname(f.pathname); if (extname !== ".app" && extname !== ".prefPane") { continue; } const app = { name: f.name.replace(/\.(app|prefPane)$/, ""), title: f.name, pathname: f.pathname, icon: null, command: null, }; app.icon = await getIcon(app.pathname, app.name); app.title = await getAppTitle(locale, app.pathname, app.name); if (!app.icon) { continue; } app.command = `open ${app.pathname.replace(/ /g, "\\ ") as string}`; appSet.add(app.pathname); apps.push(app); } return apps; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/mac/title.ts ================================================ import { Files } from "../../../../../file/main"; import { IconvUtil } from "../../../../../../lib/util"; const langDirMap = { "en-US": ["en.lproj"], "zh-CN": ["zh-Hans.lproj", "zh_CN.lproj"], }; export const getAppTitle = async ( locale: string, pathname: string, name: string, ) => { if (!(locale in langDirMap)) { return name; } const langDirs = langDirMap[locale]; // console.log('langDirs', langDirs) for (const langDir of langDirs) { const infoPlistPath = pathname + "/Contents/Resources/" + langDir + "/InfoPlist.strings"; // console.log('infoPlistPath', infoPlistPath) if (!(await Files.exists(infoPlistPath, { isDataPath: false }))) { continue; } const buffer = await Files.readBuffer(infoPlistPath, { isDataPath: false, }); const content = IconvUtil.bufferToUtf8(buffer) as string; // console.log('content', infoPlistPath, content.toString('utf8')) // CFBundleName = "网易邮箱大师"; if (content) { // console.log('content', JSON.stringify(content)) // CFBundleDisplayName = "网易邮箱大师"; const reg = new RegExp('"?CFBundleDisplayName"?.*?=.*?"(.*)".*?;'); const match = content.match(reg); if (match) { // console.log('content.result', match[1]) return match[1]; } } } // console.log('===============') // console.log('getAppTitle', locale, pathname, name) return name; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/type.ts ================================================ export type AppRecord = { name: string; title: string; pathname: string; icon: string; command: string; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/util/index.ts ================================================ import { Files } from "../../../../../file/main"; export const listFiles = async ( paths: string[], ): Promise< { name: string; pathname: string; isDirectory: boolean; size: number; lastModified: number; }[] > => { let results: any[] = []; for (const path of paths) { for (let p of await Files.list(path, { isDataPath: false, })) { results.push(p); } } return results; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/win/icon.ts ================================================ import fs from "fs"; import path from "path"; import extractFileIcon from "extract-file-icon"; import { AppEnv, waitAppEnvReady } from "../../../../../env"; const getIconTempDir = async () => { await waitAppEnvReady(); return path.join(AppEnv.dataRoot, "cache", "app-icons"); }; export const getIcon = async (appPath: string, appName: string) => { try { const iconTempDir = await getIconTempDir(); const iconPath = path.join(iconTempDir, `${appName}.png`); const iconPathUrl = `file://${iconPath}`; // console.log('iconPath', iconPath, appName, appPath); if (fs.existsSync(iconPath)) { return iconPathUrl; } const iconNone = path.join(iconTempDir, `${appName}.none`); const iconNoneUrl = `file://${iconNone}`; if (fs.existsSync(iconNone)) { return iconNoneUrl; } if (!fs.existsSync(iconTempDir)) { fs.mkdirSync(iconTempDir, { recursive: true }); } const buffer = extractFileIcon(appPath, 32); fs.writeFileSync(iconPath, buffer, "base64"); if (fs.existsSync(iconPath)) { return iconPathUrl; } else { fs.writeFileSync(iconNone, ""); } } catch (e) {} return null; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/win/index.ts ================================================ import { listFiles } from "../util"; import path from "path"; import os from "os"; import { shell } from "electron"; import { AppRecord } from "../type"; import { ShellUtil } from "../../../../../../lib/util"; import { getIcon } from "./icon"; import { getAppTitle } from "./title"; export const ManagerAppWin = { list: async () => { return lists(); }, }; const apps: AppRecord[] = []; const appSet = new Set(); const blackList = ["msiexec.exe"]; const readDir = async (dir: string) => { let files = await listFiles([dir]); for (const f of files) { if (f.isDirectory) { await readDir(f.pathname); } else { let name = f.name.split(".")[0]; let appDetail: any = {}; try { appDetail = shell.readShortcutLink(f.pathname); } catch (e) { // } const pathname = appDetail.target; if ( !pathname || appSet.has(pathname) || !pathname.endsWith(".exe") || pathname.endsWith("uninst.exe") || pathname.endsWith("uninstall.exe") ) { continue; } appSet.add(pathname); name = path.basename(appDetail.target, ".exe"); if (blackList.includes(name)) { continue; } const title = await getAppTitle("zh-CN", pathname, name); const app = { name, title, pathname, icon: await getIcon(appDetail.target, name), command: `start "dummyclient" ${ShellUtil.quotaPath(appDetail.target)}`, }; // console.log('app', app) apps.push(app); } } }; const lists = async (): Promise => { appSet.clear(); await readDir("C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs"); await readDir( path.join( os.homedir(), "./AppData/Roaming", "Microsoft\\Windows\\Start Menu\\Programs", ), ); // console.log('apps', apps) return apps; }; ================================================ FILE: electron/mapi/manager/system/plugin/app/win/title.ts ================================================ import { exec } from "child_process"; import { IconvUtil } from "../../../../../../lib/util"; export const getAppTitle = async ( locale: string, pathname: string, name: string, ) => { // (Get-ItemProperty -Path 'C:\\Program Files (x86)\\360\\360zip\\360zip.exe').VersionInfo.FileDescription // (Get-ItemProperty -Path 'C:\\Program Files (x86)\\360\\360Safe\\360Safe.exe').VersionInfo.FileDescription // (Get-ItemProperty -Path 'C:\\Windows\\SysWOW64\\msiexec.exe').VersionInfo.FileDescription const command = `powershell -Command "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; (Get-ItemProperty -Path '${pathname}').VersionInfo.FileDescription"`; return new Promise((resolve, reject) => { exec( command, { encoding: "utf-8", }, (error, stdout, stderr) => { if (error) { resolve(name); } else { // console.log('win.getAppTitle', { // locale, // pathname, // name, // stdout: stdout, // title: stdout.toString()?.trim(), // }) resolve(stdout.toString()?.trim()); } }, ); }); }; ================================================ FILE: electron/mapi/manager/system/plugin/app.ts ================================================ import { ActionMatch, ActionMatchTypeEnum, ActionRecord, ActionTypeEnum, PluginRecord, } from "../../../../../src/types/Manager"; import { t } from "../../../../config/lang"; import { isLinux, isMac, isWin } from "../../../../lib/env"; import { MemoryCacheUtil } from "../../../../lib/util"; import { ManagerFileCacheUtil } from "../../lib/cache"; import { Manager } from "../../manager"; import { SystemIcons } from "../asset/icon"; import { ManagerSystem } from "../index"; import { ManagerAppLinux } from "./app/linux"; import { ManagerAppMac } from "./app/mac"; import { AppRecord } from "./app/type"; import { ManagerAppWin } from "./app/win"; let logo = SystemIcons.windows; if (isMac) { logo = SystemIcons.apple; } else if (isLinux) { logo = SystemIcons.linux; } export const AppPlugin: PluginRecord = { name: "app", title: t("system.apps"), version: "1.0.0", logo: logo, description: t("system.appsDesc"), main: null, preload: null, actions: [], }; const list = async () => { let apps: AppRecord[] = []; if (isMac) { apps = await ManagerAppMac.list(); } else if (isWin) { apps = await ManagerAppWin.list(); } else if (isLinux) { apps = await ManagerAppLinux.list(); } return apps; }; const listActions = async () => { // await sleep(3500) return await MemoryCacheUtil.remember("AppActions", async () => { const actions: ActionRecord[] = []; const apps = await list(); apps.forEach((app) => { const matches: ActionMatch[] = []; matches.push({ type: ActionMatchTypeEnum.TEXT, text: app.name, } as ActionMatch); if (app.title !== app.name) { matches.push({ type: ActionMatchTypeEnum.TEXT, text: app.title, } as ActionMatch); } actions.push({ fullName: `${AppPlugin.name}/${app.name}`, pluginName: AppPlugin.name, name: app.name, title: app.title, icon: app.icon, type: ActionTypeEnum.COMMAND, matches: matches, data: { command: app.command, }, }); }); // console.log('actions', actions) return actions; }); }; type ActionInfo = { time: number; actions: ActionRecord[]; }; let listActionRunning = false; let listActionFirstRunning = true; export const getAppPlugin = async () => { AppPlugin.actions = []; let toastTimer = null; const cacheInfo = await ManagerFileCacheUtil.getIgnoreExpire( "AppActions", [], ); AppPlugin.actions = cacheInfo.value; let shouldNotice = false; if (!cacheInfo.isCache || cacheInfo.expire < Date.now()) { if (!listActionRunning) { listActionRunning = true; if (listActionFirstRunning) { listActionFirstRunning = false; shouldNotice = true; } listActions().then((actions) => { // console.log('find.actions', actions) AppPlugin.actions = actions; ManagerFileCacheUtil.set("AppActions", actions, 1000 * 3600); if (toastTimer) { clearTimeout(toastTimer); } else if (shouldNotice) { Manager.setNotice({ text: t("system.appsIndexed"), type: "success", duration: 5000, }).then(); } listActionRunning = false; ManagerSystem.clearCache(); }); } } if (!AppPlugin.actions.length && shouldNotice) { toastTimer = setTimeout(() => { Manager.setNotice(t("system.appsIndexing")).then(); toastTimer = null; }, 3000); } return AppPlugin; }; ================================================ FILE: electron/mapi/manager/system/plugin/file.ts ================================================ import { ActionMatch, ActionMatchTypeEnum, ActionRecord, ActionTypeEnum, FilePluginRecord, PluginRecord, } from "../../../../../src/types/Manager"; import { CommonConfig } from "../../../../config/common"; import { t } from "../../../../config/lang"; import { MemoryCacheUtil, ShellUtil } from "../../../../lib/util"; import { KVDBMain } from "../../../kvdb/main"; import { SystemIcons } from "../asset/icon"; import { ManagerSystem } from "../index"; export const FilePlugin: PluginRecord = { name: "file", title: t("system.fileLaunch"), version: "1.0.0", logo: SystemIcons.folder, description: t("system.fileLaunchDesc"), main: null, preload: null, actions: [], }; const listActions = async () => { return await MemoryCacheUtil.remember( "FileActions", async () => { const actions: ActionRecord[] = []; const records = await ManagerSystemPluginFile.list(); records.forEach((record, recordIndex) => { actions.push({ fullName: `${FilePlugin.name}/${record.title}`, pluginName: FilePlugin.name, name: record.title, title: record.title, icon: record.icon, type: ActionTypeEnum.COMMAND, matches: [ { type: ActionMatchTypeEnum.TEXT, text: record.title, } as ActionMatch, ], data: { command: "open " + ShellUtil.quotaPath(record.path), }, }); }); return actions; }, ); }; export const getFilePlugin = async () => { FilePlugin.actions = await listActions(); return FilePlugin; }; export const ManagerSystemPluginFile = { async list(): Promise { return MemoryCacheUtil.remember("Files", async () => { const res = await KVDBMain.getData( CommonConfig.dbSystem, CommonConfig.dbFileId, ); if (res) { return res["records"] || []; } return []; }); }, async update(records: FilePluginRecord[]) { await KVDBMain.putForce(CommonConfig.dbSystem, { _id: CommonConfig.dbFileId, records: records, }); MemoryCacheUtil.forget("Files"); MemoryCacheUtil.forget("FileActions"); await ManagerSystem.clearCache(); }, }; ================================================ FILE: electron/mapi/manager/system/plugin/store/action.ts ================================================ import { ActionTypeCodeData } from "../../../../../../src/types/Manager"; import { screenCapture } from "../../../plugin/screenCapture"; import { AppsMain } from "../../../../app/main"; export const StoreActionCode = {}; ================================================ FILE: electron/mapi/manager/system/plugin/store/index.ts ================================================ import { PluginType } from "../../../../../../src/types/Manager"; import { Files } from "../../../../file/main"; import { UserApi } from "../../../../user/main"; import { Manager } from "../../../manager"; import { ManagerPlugin } from "../../../plugin"; // @ts-ignore import fs from "node:fs"; import { resolve } from "node:path"; import { mapError } from "../../../../../../src/lib/error"; import { t } from "../../../../../config/lang"; import { MarkdownUtil } from "../../../../../lib/util"; import { AppsMain } from "../../../../app/main"; import { Misc } from "../../../../misc"; export const ManagerPluginStore = { installingMap: {} as { [pluginName: string]: { name: string; percent: number; startTime: number; }; }, async install( pluginName: string, option?: { version?: string; }, ) { this.installingMap[pluginName] = { name: pluginName, percent: 0, startTime: Date.now(), }; option = Object.assign( { version: null, }, option, ); const payload = { plugin: pluginName, version: option["version"], }; const existPlugin = await ManagerPlugin.get(pluginName); let isUpgrade = false; if (existPlugin && existPlugin.version !== option["version"]) { isUpgrade = true; } try { if (isUpgrade) { await ManagerPlugin.uninstall(pluginName); } const infoRes = await UserApi.post( "store/plugin_info_guest", payload, ); await ManagerPlugin.configCheck(infoRes.data["config"]); // console.log('ManagerPluginStore.install', JSON.stringify({pluginName, option, data: infoRes.data}, null, 2)); const packageRes = await UserApi.post( "store/plugin_package_guest", payload, ); const packageUrl = packageRes.data["package"]; const packageMd5 = packageRes.data["packageMd5"]; // console.log('ManagerPluginStore.install', JSON.stringify({pluginName, option, packageRes}, null, 2)); const tempFile = await Files.temp("zip"); // console.log('ManagerPluginStore.install', JSON.stringify({pluginName, option, tempFile}, null, 2)); // console.log('ManagerPluginStore.install.downloadStart'); let lastPercent = 0; await Files.download(packageUrl, tempFile, { isDataPath: false, progress(percent, total) { const p = Math.floor(percent * 100 * 0.99); if (lastPercent != p) { lastPercent = p; // console.log('ManagerPluginStore.install.downloadProgress', {p, total}); Manager.sendBroadcast( "store", "PluginInstallProgress", { pluginName: pluginName, percent: p, end: false, }, ); if (ManagerPluginStore.installingMap[pluginName]) { ManagerPluginStore.installingMap[ pluginName ].percent = p; } } }, }); // sleep 500 await new Promise((resolve) => setTimeout(resolve, 500)); // console.log('ManagerPluginStore.install.downloadEnd'); // console.log('ManagerPluginStore.install.start'); await ManagerPlugin.installFromFileOrDir( tempFile, PluginType.STORE, ); // console.log('ManagerPluginStore.install.end'); AppsMain.toast( t("plugin.installComplete", { title: infoRes.data["config"]["title"], }), { status: "success", }, ); Manager.sendBroadcast("store", "PluginInstallProgress", { pluginName: pluginName, percent: 100, end: true, }); } catch (e) { throw mapError(e); } finally { delete this.installingMap[pluginName]; } }, async publish( pluginName: string, option?: { version?: string; }, ) { option = Object.assign( { version: null, }, option, ); const plugin = await Manager.getPlugin(pluginName); if (!plugin) { throw "PluginNotExists"; } if (plugin.type !== PluginType.DIR) { throw "PluginNotPublishAble"; } if (plugin.version !== option["version"]) { throw "PublishVersionNotMatch"; } if (!plugin.runtime.root) { throw "PluginNotPublishAble"; } const root = plugin.runtime.root; const config = await Files.read(resolve(root, "config.json"), { isDataPath: false, }); if (!config) { throw "PluginFormatError:-9"; } let configJson = null; try { configJson = JSON.parse(config); } catch (e) {} if (!configJson) { throw "PluginFormatError:-10"; } if (configJson["name"] !== pluginName) { throw "PluginFormatError:-11"; } const payload = { plugin: pluginName, version: option["version"], feature: null, content: null, package: null, }; configJson["development"] = configJson["development"] || {}; if (configJson["development"]["env"] === "dev") { throw "PluginEnvError"; } configJson["development"]["releaseDoc"] = configJson["development"]["releaseDoc"] || "release.md"; const releaseDocPath = resolve( root, configJson["development"]["releaseDoc"], ); // console.log('releaseDocPath', releaseDocPath) const releaseDoc = await Files.read(releaseDocPath, { isDataPath: false, }); if (releaseDoc) { const parts = releaseDoc.split("---"); for (const part of parts) { let lines = part.split("\n"); while (!payload.feature && lines.length) { const line = lines.shift(); // ## x.x.x 功能特性 if (line.startsWith("##")) { const parts = line.split(" "); if (parts.length === 3) { if (parts[1] === payload.version) { payload.feature = parts[2]; payload.content = MarkdownUtil.toHtml( lines.join("\n").trim(), ); break; } } } } if (payload.feature) { break; } } } if (!payload.feature || !payload.content) { if (!releaseDoc) { throw "PluginReleaseDocNotFound"; } if (!payload.feature) { throw "PluginReleaseDocFormatError:-1"; } throw "PluginReleaseDocFormatError:-2"; } const pluginInfo = await this._getPluginInfo(root, configJson); const tempFile = await Files.temp("zip"); await Misc.zip(tempFile, plugin.runtime.root, { filter: async (entry) => { if (entry.isDir) { if (["node_modules", ".git"].includes(entry.name)) { return false; } if ( await Files.exists(resolve(entry.fullPath, ".faignore")) ) { return false; } } return true; }, end: async (archive: any) => { delete configJson["development"]; delete configJson["$schema"]; archive.append(JSON.stringify(configJson, null, 4), { name: "config.json", }); }, }); if (!fs.existsSync(tempFile)) { throw "PluginZipError"; } // console.log('tempFile', tempFile) const buffer = await Files.readBuffer(tempFile, { isDataPath: false, }); payload.package = buffer.toString("base64"); await Files.deletes(tempFile, { isDataPath: false, }); return await UserApi.post("store/plugin_publish", { ...payload, ...pluginInfo, }); }, async publishInfo( pluginName: string, option?: { version?: string; }, ) { option = Object.assign( { version: null, }, option, ); const plugin = await Manager.getPlugin(pluginName); if (!plugin) { throw "PluginNotExists"; } if (plugin.type !== PluginType.DIR) { throw "PluginNotPublishAble"; } if (plugin.version !== option["version"]) { throw "PublishVersionNotMatch"; } if (!plugin.runtime.root) { throw "PluginNotPublishAble"; } const root = plugin.runtime.root; const config = await Files.read(resolve(root, "config.json"), { isDataPath: false, }); if (!config) { throw "PluginFormatError:-12"; } let configJson = null; try { configJson = JSON.parse(config); } catch (e) {} if (!configJson) { throw "PluginFormatError:-13"; } const payload = { plugin: pluginName, version: option["version"], }; const pluginInfo = await this._getPluginInfo(root, configJson); return await UserApi.post("store/plugin_publish_info", { ...payload, ...pluginInfo, }); }, async storeInstallingInfo(pluginName: string) { const result = { isInstalling: false, percent: 0, }; if (this.installingMap[pluginName]) { result.isInstalling = true; result.percent = this.installingMap[pluginName].percent; } return result; }, async _getPluginInfo(root: string, configJson: any) { const result = { pluginContent: null, pluginPreview: null, }; configJson["development"] = configJson["development"] || {}; configJson["development"]["contentDoc"] = configJson["development"]["contentDoc"] || "content.md"; const contentDocPath = resolve( root, configJson["development"]["contentDoc"], ); const contentDoc = await Files.read(contentDocPath, { isDataPath: false, }); if (contentDoc) { result.pluginContent = MarkdownUtil.toHtml(contentDoc); } configJson["development"]["previewDoc"] = configJson["development"]["previewDoc"] || "preview.md"; const previewDocPath = resolve( root, configJson["development"]["previewDoc"], ); const previewDoc = await Files.read(previewDocPath, { isDataPath: false, }); if (previewDoc) { const images = []; previewDoc.split("\n").forEach((line: string) => { // https://example.com/path/to/image.png // ![image](https://example.com/path/to/image.png) const match = line.match(/!\[.*?\]\((.*?)\)/); if (match) { images.push(match[1].trim()); } else { images.push(line.trim()); } }); result.pluginPreview = JSON.stringify( images.filter((url) => !!url), ); } return result; }, }; // setTimeout(() => { // ManagerPluginStore.publishInfo('AxxxdDddd', { // version: '1.2.0', // }) // }, 3000) ================================================ FILE: electron/mapi/manager/system/plugin/store.ts ================================================ import { ActionTypeEnum, PluginRecord } from "../../../../../src/types/Manager"; import { t } from "../../../../config/lang"; import { SystemIcons } from "../asset/icon"; export const StorePlugin: PluginRecord = { name: "store", title: t("plugin.market"), version: "1.0.0", logo: SystemIcons.pluginStore, description: t("system.storeDesc"), main: "/page/store.html", preload: "", actions: [ { name: "default", title: t("plugin.market"), type: ActionTypeEnum.WEB, icon: SystemIcons.pluginStore, matches: [t("plugin.market"), "store"] as any, }, ], }; ================================================ FILE: electron/mapi/manager/system/plugin/system/action.ts ================================================ import os from "os"; import { ActionTypeCodeData } from "../../../../../../src/types/Manager"; import { t } from "../../../../../config/lang"; import { isLinux, isMac, isWin } from "../../../../../lib/env"; import { Page } from "../../../../../page"; import { AppsMain } from "../../../../app/main"; import { AppRuntime } from "../../../../env"; import { KeyboardKey, ManagerHotkeySimulate } from "../../../hotkey/simulate"; import { colorPicker } from "../../../plugin/colorPicker"; import { screenCapture } from "../../../plugin/screenCapture"; import { screenRecord } from "../../../plugin/screenRecord"; export const SystemActionCode = { screenshot: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); screenCapture((image: string) => { AppsMain.setClipboardImage(image); }); }, colorPicker: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); colorPicker().then(); }, screenRecord: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); screenRecord().then(); }, guide: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); await Page.open("guide", {}); }, about: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); await Page.open("about", {}); }, lock: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); if (isMac) { ManagerHotkeySimulate.keyTap(KeyboardKey.Q, [ KeyboardKey.Meta, KeyboardKey.Ctrl, ]); } else if (isWin) { ManagerHotkeySimulate.keyTap(KeyboardKey.L, [KeyboardKey.Meta]); } else if (isLinux) { ManagerHotkeySimulate.keyTap(KeyboardKey.L, [KeyboardKey.Meta]); } }, ip: async (data: ActionTypeCodeData) => { AppRuntime.mainWindow.hide(); const ip = getLocalIPAddress(); AppsMain.setClipboardText(ip); AppsMain.toast(t("system.ipCopied", { ip })); }, }; function getLocalIPAddress() { const networkInterfaces = os.networkInterfaces(); for (const interfaceName in networkInterfaces) { const interfaces = networkInterfaces[interfaceName]; for (const iface of interfaces) { if (iface.family === "IPv4" && !iface.internal) { return iface.address; } } } return "127.0.0.1"; } ================================================ FILE: electron/mapi/manager/system/plugin/system.ts ================================================ import { ActionTypeEnum, PluginRecord } from "../../../../../src/types/Manager"; import { t } from "../../../../config/lang"; import { SystemIcons } from "../asset/icon"; export const SystemPlugin: PluginRecord = { name: "system", title: t("system.title"), version: "1.0.0", logo: SystemIcons.pluginSystem, description: t("system.desc"), main: "/page/system.html", preload: "", actions: [ { name: "page-data", title: t("system.dataCenter"), type: ActionTypeEnum.WEB, icon: SystemIcons.database, matches: [t("system.dataCenter"), "data"] as any, }, { name: "page-setting", title: t("system.functionSettings"), type: ActionTypeEnum.WEB, icon: SystemIcons.pluginSystem, matches: [t("system.functionSettings"), "setting"] as any, }, { name: "page-plugin", title: t("system.pluginManagement"), type: ActionTypeEnum.WEB, icon: SystemIcons.plugin, matches: [t("system.pluginManagement"), "plugin"] as any, }, { name: "page-action", title: t("system.actionManagement"), type: ActionTypeEnum.WEB, icon: SystemIcons.command, matches: [t("system.actionManagement"), "action"] as any, }, { name: "page-file", title: t("system.fileLaunch"), type: ActionTypeEnum.WEB, icon: SystemIcons.folder, matches: [t("system.fileLaunch"), "file"] as any, }, { name: "page-launch", title: t("system.hotkeys"), type: ActionTypeEnum.WEB, icon: SystemIcons.thunder, matches: [t("system.hotkeys"), "launch"] as any, }, { name: "about", title: t("system.about"), type: ActionTypeEnum.CODE, icon: SystemIcons.about, matches: [t("system.about"), "about"] as any, }, { name: "screenshot", title: t("system.screenshot"), type: ActionTypeEnum.CODE, icon: SystemIcons.screenshot, matches: [t("system.screenshot"), "screenshot", "snapshot"] as any, }, { name: "colorPicker", title: t("system.colorPicker"), type: ActionTypeEnum.CODE, icon: SystemIcons.colorPicker, matches: [t("system.colorPicker"), "ColorPicker"] as any, }, { name: "screenRecord", title: t("system.screenRecord"), type: ActionTypeEnum.CODE, icon: SystemIcons.screenRecord, matches: [t("system.screenRecord"), "ScreenRecord"] as any, }, { name: "guide", title: t("nav.guide"), type: ActionTypeEnum.CODE, icon: SystemIcons.guide, matches: [t("nav.guide"), "guide"] as any, }, { name: "lock", title: t("system.lockScreen"), type: ActionTypeEnum.CODE, icon: SystemIcons.lock, matches: [t("system.lockScreen"), "lock"] as any, }, { name: "ip", title: t("system.lanIP"), type: ActionTypeEnum.CODE, icon: SystemIcons.ip, matches: [t("system.lanIP")] as any, }, ], }; ================================================ FILE: electron/mapi/manager/type.ts ================================================ import { BrowserView, BrowserWindow } from "electron"; import { ActiveWindow, PluginRecord } from "../../../src/types/Manager"; export type PluginContext = (BrowserView | {}) & { _plugin: PluginRecord; _window?: BrowserWindow; _event?: { [key: string]: any[]; }; }; export type SearchQuery = { keywords: string; currentFiles?: FileItem[]; currentImage?: string; currentText?: string; activeWindow?: ActiveWindow; }; ================================================ FILE: electron/mapi/manager/window/index.ts ================================================ import * as remoteMain from "@electron/remote/main"; import { BrowserView, BrowserWindow, screen, shell, WebContents, } from "electron"; import { ActionRecord, PluginRecord, PluginState, } from "../../../../src/types/Manager"; import { t } from "../../../config/lang"; import { WindowConfig } from "../../../config/window"; import { DevToolsManager } from "../../../lib/devtools"; import { isMac } from "../../../lib/env"; import { preloadDefault, rendererIsUrl, rendererLoadPath, } from "../../../lib/env-main"; import { HotKeyUtil } from "../../../lib/util"; import { AppsMain } from "../../app/main"; import { AppEnv, AppRuntime } from "../../env"; import { Events } from "../../event/main"; import { Log } from "../../log/main"; import { executeDarkMode, executeHooks, executePluginHooks, } from "../lib/hooks"; import { ManagerPlugin } from "../plugin"; import { ManagerPluginEvent } from "../plugin/event"; import { PluginLog } from "../plugin/log"; import { ManagerSystem } from "../system"; import { PluginContext } from "../type"; import { RemoteWebManager } from "./remoteWeb"; const browserViews = new Map(); const detachWindows = new Map(); let mainWindowView: BrowserView | null = null; const mainPluginActionCode = { view: null as BrowserView | null, action: null as ActionRecord | null, codeData: null, items: [] as { id: string; [key: string]: any; }[], }; type OpenOptionType = { type: "action" | "callPage"; callPage?: { type: string; data: any; option: CallPageOption; onResult: (result: { code: number; msg: string; data?: any }) => void; }; }; type OpenShowWindowOption = { loadUrl: () => void; pluginState: PluginState; width: number; height: number; option: OpenOptionType; }; const addBrowserViews = (view: BrowserView) => { browserViews.set(view.webContents, view); }; const removeBrowserViews = (view: BrowserView) => { browserViews.delete(view.webContents); }; const addDetachWindows = (win: BrowserWindow) => { detachWindows.set(win.webContents, win); }; const removeDetachWindows = (win: BrowserWindow) => { detachWindows.delete(win.webContents); }; const checkForHotkey = async (view: PluginContext, input: Electron.Input) => { if (view._event && view._event["Hotkey"]) { const hotkey = HotKeyUtil.getFromEvent(input); if (hotkey) { view._event["Hotkey"].forEach(({ id, hotkeys }) => { if (HotKeyUtil.match(hotkeys, hotkey)) { executePluginHooks(view as BrowserView, "Hotkey", { id, hotkey, }); } }); } } }; export const ManagerWindow = { listBrowserViews(): BrowserView[] { return Array.from(browserViews.values()); }, listDetachWindows(): BrowserWindow[] { return Array.from(detachWindows.values()); }, getViewByWebContents: (webContents: any) => { // console.log('getViewByWebContents.value', webContents) let view = browserViews.get(webContents); if (view) { return view; } const iterator = browserViews.entries(); while (true) { const { value, done } = iterator.next(); if (done) { break; } // console.log('getViewByWebContents.value.start', value[1], value[1]._window) if (value[1]._window.webContents === webContents) { return value[1]; } } return null; }, async detachWindowOperate(type: "open" | "close", action: ActionRecord) { let win = null; for (const w of ManagerWindow.listDetachWindows()) { if (w.id === action.runtime.windowId) { win = w; break; } } if (!win) { throw "DetachWindowNotFound"; } if (type === "open") { win.show(); win.focus(); } else { win.close(); } AppRuntime.mainWindow.setSize( WindowConfig.mainWidth, WindowConfig.mainHeight, ); setTimeout(() => { AppRuntime.mainWindow.hide(); }, 100); }, async _logPluginViewError(view: BrowserView, plugin: PluginRecord) { view.webContents.on( "did-fail-load", (event, errorCode, errorDescription, validatedURL) => { PluginLog.error(plugin.name, "Load.Error-did-fail-load", { errorCode, errorDescription, validatedURL, }); }, ); view.webContents.on( "did-fail-provisional-load", (event, errorCode, errorDescription, validatedURL) => { PluginLog.error( plugin.name, "Load.Error-did-fail-provisional-load", { errorCode, errorDescription, validatedURL, }, ); }, ); view.webContents.on("preload-error", (event, preloadPath, error) => { PluginLog.error(plugin.name, "Load.Error-preload-error", { error: error + "", preloadPath, }); }); view.webContents.on("render-process-gone", () => { PluginLog.error(plugin.name, "Load.Error-render-process-gone", { error: "render-process-gone", }); }); }, async _pluginViewLoad(view: BrowserView, main: string) { try { if (rendererIsUrl(main)) { await view.webContents.loadURL(main); } else { await view.webContents.loadFile(main); } } catch (e) { view.webContents.loadURL("about:blank").then(); PluginLog.error(view._plugin.name, "Load.Error-loadUrl", { error: e + "", main, }); } }, async _pluginActionCodeEnd() { if (mainPluginActionCode.view) { AppRuntime.mainWindow.removeBrowserView(mainPluginActionCode.view); removeBrowserViews(mainPluginActionCode.view); if ( ManagerPlugin.isDevelopmentCheck( mainPluginActionCode.view._plugin, "keepCodeDevTools", ) ) { PluginLog.info( mainPluginActionCode.view._plugin.name, "ManagerWindow.KeepCodeDevTools", { action: mainPluginActionCode.action, codeData: mainPluginActionCode.codeData, }, ); } else { // @ts-ignore mainPluginActionCode.view.webContents?.destroy(); mainPluginActionCode.view = null; } } mainPluginActionCode.action = null; mainPluginActionCode.codeData = null; mainPluginActionCode.items = []; }, async _viewCodeCallJs(js: string) { return await mainPluginActionCode.view.webContents.executeJavaScript( `(async()=>{ ${js} })();`, ); }, async actionCodeExecute( id: string | null = null, keywords: string | null = null, ) { let item: ActionCodeExecuteResultItem | null = null; if (id) { item = mainPluginActionCode.items.find( (i) => i.id === id, ) as ActionCodeExecuteResultItem; } try { let hasLoading = false; if (!(item && "loading" in item && !item["loading"])) { await executeHooks(AppRuntime.mainWindow, "PluginCodeSetting", { loading: true, }); hasLoading = true; } let value: ActionCodeExecuteResult = await this._viewCodeCallJs( `return await window.exports.code['${mainPluginActionCode.action.name}'].execute( ${JSON.stringify(item)}, ${JSON.stringify(keywords)}, ${JSON.stringify(mainPluginActionCode.codeData)} );`, ); if (!value) { value = { command: "none" } as ActionCodeExecuteResult; } if (hasLoading) { await executeHooks(AppRuntime.mainWindow, "PluginCodeSetting", { loading: false, }); } // console.log('ManagerWindow.openActionCode.value', JSON.stringify(value)) const plugin: PluginRecord = mainPluginActionCode.view._plugin; if (value.placeholder) { await executeHooks(AppRuntime.mainWindow, "PluginCodeSetting", { placeholder: value.placeholder, }); } if ("data" === value.command) { mainPluginActionCode.items = value.items || []; // icon path mainPluginActionCode.items.forEach((item) => { if ( item.icon && !item.icon.startsWith("http:") && !item.icon.startsWith("file:") && !item.icon.startsWith("data:") ) { item.icon = `file://${plugin.runtime.root}/${item.icon}`; } }); await executeHooks(AppRuntime.mainWindow, "PluginCodeData", { items: value.items, }); } else if ("close" === value.command) { await this.close(); AppRuntime.mainWindow.hide(); } else if ("error" === value.command) { await executeHooks(AppRuntime.mainWindow, "PluginCodeSetting", { error: value.error, }); } else if ("clear" === value.command) { await this.close(); } else if ("none" === value.command) { // do nothing } else { throw `ManagerWindow.OpenActionCode.CommandError:${value.command}`; } } catch (e) { await executeHooks(AppRuntime.mainWindow, "PluginCodeSetting", { error: e + "", }); PluginLog.error( mainPluginActionCode.view._plugin.name, "Code.Error", { error: e + "", action: mainPluginActionCode.action, }, ); } }, async openForCode( plugin: PluginRecord, action: ActionRecord, option?: { codeData?: any; }, ) { const { nodeIntegration, preloadBase, preload, main } = await ManagerPlugin.getInfo(plugin); // console.log('openForCode', {preload, main}) const viewSession = await ManagerPlugin.getViewSession(plugin); if (preloadBase) { viewSession.setPreloads([preloadBase]); } const view = new BrowserView({ webPreferences: { webSecurity: false, nodeIntegration, contextIsolation: false, sandbox: false, devTools: true, webviewTag: true, preload, session: viewSession, defaultFontSize: 14, defaultFontFamily: { standard: "system-ui", serif: "system-ui", }, spellcheck: false, }, }); mainPluginActionCode.view = view; mainPluginActionCode.action = action; mainPluginActionCode.codeData = option?.codeData || null; await ManagerWindow._logPluginViewError(view, plugin); addBrowserViews(view); view._plugin = plugin; view._window = AppRuntime.mainWindow; remoteMain.enable(view.webContents); AppRuntime.mainWindow.addBrowserView(view); ManagerWindow._pluginViewLoad(view, main).then(); DevToolsManager.register(`MainCodeView.${plugin.name}`, view); const logPluginError = (e) => { PluginLog.error(plugin.name, "Code.Error", { error: e + "", action, option, }); }; const endView = () => { setTimeout(() => { this._pluginActionCodeEnd(); }, 1000); AppRuntime.mainWindow.hide(); }; AppRuntime.mainWindow.setSize( WindowConfig.pluginWidth, WindowConfig.mainHeight, ); return new Promise((resolve, reject) => { view.webContents.once("dom-ready", async () => { DevToolsManager.autoShow(view); if ( ManagerPlugin.isDevelopmentCheck(plugin, "showCodeDevTools") ) { view.webContents.openDevTools({ mode: "detach", activate: true, title: `MainPluginCodeView.${plugin.name}`, }); } view.setBounds({ x: 0, y: 0, width: 0, height: 0, }); try { const codeType = await this._viewCodeCallJs( `return typeof window.exports.code['${action.name}'];`, ); if ("function" === codeType) { const value = await this._viewCodeCallJs( `return await window.exports.code['${action.name}'](${JSON.stringify(mainPluginActionCode.codeData)});`, ); resolve(value); endView(); } else { const codeSetting = await this._viewCodeCallJs( `return window.exports.code['${action.name}'].setting;`, ); if (!codeSetting) { throw `ManagerWindow.OpenForCode.SettingEmpty`; } await executeHooks( AppRuntime.mainWindow, "PluginCodeInit", { plugin: plugin, type: codeSetting.type || "list", placeholder: codeSetting.placeholder || t("store.searchPlaceholder"), }, ); this.actionCodeExecute().then(); resolve(null); } } catch (e) { logPluginError(e); reject(e); endView(); } }); }); }, async open( plugin: PluginRecord, action?: ActionRecord, option?: OpenOptionType, ) { option = Object.assign( { type: "action", callPage: {}, }, option, ); const { nodeIntegration, preloadBase, preload, main, width, height, autoDetach, singleton, zoom, } = await ManagerPlugin.getInfo(plugin); // console.log('ManagerWindow.open', {nodeIntegration, preload, main, width, height, autoDetach}) const readyData = {}; readyData["actionName"] = action?.name || null; readyData["actionMatch"] = action?.runtime?.match || null; readyData["actionMatchFiles"] = action?.runtime?.matchFiles || []; readyData["requestId"] = action?.runtime?.requestId || null; readyData["reenter"] = false; readyData["isView"] = false; readyData["type"] = option.type; if (option.type === "action" && singleton) { for (const v of this.listBrowserViews()) { if (v._plugin.name === plugin.name) { v._window.show(); v._window.focus(); await executeHooks( AppRuntime.mainWindow, "PluginAlreadyOpened", {}, ); readyData["reenter"] = true; await executePluginHooks(v, "PluginReady", readyData); return; } } } const viewSession = await ManagerPlugin.getViewSession(plugin); if (preloadBase) { viewSession.setPreloads([preloadBase]); } if (plugin.setting.remoteWebCacheEnable) { await RemoteWebManager.create(plugin); } // console.log('preload', {preloadPluginDefault, preload}) const view = new BrowserView({ webPreferences: { webSecurity: false, nodeIntegration, contextIsolation: false, allowRunningInsecureContent: true, sandbox: false, devTools: true, webviewTag: true, preload, session: viewSession, defaultFontSize: 14, defaultFontFamily: { standard: "system-ui", serif: "system-ui", }, spellcheck: false, }, }); await ManagerWindow._logPluginViewError(view, plugin); addBrowserViews(view); view._plugin = plugin; remoteMain.enable(view.webContents); DevToolsManager.register(`PluginView.${plugin.name}`, view); view.webContents.once("dom-ready", async () => { await executeDarkMode(view, { plugin, isSystem: ManagerSystem.match(plugin.name), }); Events.sendRaw(view.webContents, "APP_READY", { name: plugin.name, AppEnv, }); }); view.webContents.once("did-frame-finish-load", () => { // console.log('setZoomFactor', zoom / 100) setTimeout(() => { view.webContents.setZoomFactor(zoom / 100); }, 0); }); view.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith("https://") || url.startsWith("http://")) { shell.openExternal(url); } return { action: "deny" }; }); view.setAutoResize({ width: true, height: true }); // console.log('ManagerWindow.open', {nodeIntegration, preload, main, width, height, autoDetach}) view.webContents.once("dom-ready", async () => { DevToolsManager.autoShow(view); if (ManagerPlugin.isDevelopmentCheck(plugin, "showDevTools")) { view.webContents.openDevTools({ mode: "detach", activate: true, title: `PluginView.${plugin.name}`, }); } if (option.type === "callPage") { Events.callPage( view.webContents, option.callPage.type, option.callPage.data, option.callPage.option, ) .then((result) => { option.callPage.onResult(result); }) .catch((e) => { option.callPage.onResult({ code: -1, msg: e + "" }); }) .finally(() => { if (option.callPage.option.autoClose) { setTimeout(() => { view._window.close(); }, 1000); } }); readyData["isView"] = true; } }); view.webContents.on("before-input-event", (event, input) => { // console.log('Load.Error-before-input-event', input) if (input.type === "keyUp") { // exit when Escape key is pressed if (mainWindowView === view) { if (input.key === "Escape") { if ( !( input.meta || input.control || input.shift || input.alt ) ) { if (mainWindowView) { ManagerWindow.close(); AppRuntime.mainWindow.webContents.focus(); } } } } else { if (input.key === "Escape") { if ( !( input.meta || input.control || input.shift || input.alt ) ) { view._window.isFullScreen() && view._window.setFullScreen(false); } } } } else if (input.type === "keyDown") { checkForHotkey(view as any, input); } }); const windowOption: OpenShowWindowOption = { width, height, pluginState: { value: "", placeholder: "", isVisible: false, }, loadUrl: async () => { ManagerWindow._pluginViewLoad(view, main).then(); }, option, }; setTimeout(async () => { if (autoDetach) { if (!mainWindowView) { AppRuntime.mainWindow.hide(); } } if (autoDetach || option.type === "callPage") { await this._showInDetachWindow(view, windowOption); } else { await this._showInMainWindow(view, windowOption); } // Log.info('open.PluginReady', JSON.stringify({readyData, action})) await executePluginHooks(view, "PluginReady", readyData); }, 0); }, async subInputChange(win: BrowserWindow, keywords: string) { const views = win.getBrowserViews(); for (const view of views) { if (AppRuntime.mainWindow === win && view !== mainWindowView) { continue; } await executePluginHooks(view, "SubInputChange", keywords); } }, async close( plugin?: PluginRecord, option?: { window?: BrowserWindow; openForNext?: boolean; }, ) { option = Object.assign( { openForNext: false, }, option, ); if ( mainWindowView && (!plugin || mainWindowView._plugin.name === plugin.name) ) { await executePluginHooks(mainWindowView, "PluginExit", null).then(); await executeHooks(AppRuntime.mainWindow, "PluginExit", { openForNext: option.openForNext, }); removeBrowserViews(mainWindowView); AppRuntime.mainWindow.removeBrowserView(mainWindowView); // @ts-ignore mainWindowView.webContents?.destroy(); mainWindowView = null; } else if ( mainPluginActionCode.view && (!plugin || mainPluginActionCode.view._plugin.name === plugin.name) ) { await executeHooks(AppRuntime.mainWindow, "PluginCodeExit", {}); await this._pluginActionCodeEnd(); } else { // detach的插件窗口 if (option.window) { option.window.close(); } else { Log.error("ManagerWindow.close", "windowNotFound"); } } }, async openMainPluginDevTools(option?: {}) { const devToolsWin = DevToolsManager.getWindow(mainWindowView); if (devToolsWin) { devToolsWin.close(); } else if (mainWindowView) { if (mainWindowView.webContents.isDevToolsOpened()) { mainWindowView.webContents.closeDevTools(); } else { mainWindowView.webContents.openDevTools({ mode: "detach", activate: true, title: `MainPluginView`, }); } } else if (mainPluginActionCode.view) { if (mainPluginActionCode.view.webContents.isDevToolsOpened()) { mainPluginActionCode.view.webContents.closeDevTools(); } else { mainPluginActionCode.view.webContents.openDevTools({ mode: "detach", activate: true, title: `MainPluginCodeView.${mainPluginActionCode.view._plugin.name}`, }); } } else { Log.error( "ManagerWindow.openMainPluginDevTools", "mainWindowViewNotFound", ); } }, async _showInMainWindow(view: BrowserView, option: OpenShowWindowOption) { if (!(await ManagerPluginEvent.isMainWindowShown(null, null))) { await ManagerPluginEvent.showMainWindow(null, null); } // console.log('showInMainWindow', view._plugin.name, option) if (mainWindowView) { await this.close(mainWindowView._plugin, { openForNext: true, }); mainWindowView = null; } view._window = AppRuntime.mainWindow; mainWindowView = view; AppRuntime.mainWindow.addBrowserView(view); AppRuntime.mainWindow.setSize( option.width, WindowConfig.mainHeight + option.height, ); const pluginParam = {}; const pluginState: PluginState = { value: "", placeholder: "", isVisible: false, }; const pluginInitReadyParam = { plugin: view._plugin, state: pluginState, param: pluginParam, }; await executeHooks(view._window, "PluginInit", pluginInitReadyParam); view.webContents.once("dom-ready", async () => { await executeHooks( view._window, "PluginInitReady", pluginInitReadyParam, ); view.setBounds({ x: 0, y: WindowConfig.mainHeight, width: option.width, height: option.height, }); AppRuntime.mainWindow.focus(); }); option.loadUrl(); }, async _showInDetachWindow(view: BrowserView, option: OpenShowWindowOption) { const plugin = view._plugin; let alwaysOnTop = false; if (plugin.setting?.detachAlwaysOnTop) { alwaysOnTop = true; } const { x, y } = AppsMain.calcPositionInCurrentDisplay( plugin.setting?.detachPosition || "center", option.width, option.height + WindowConfig.detachWindowTitleHeight, ); let win = new BrowserWindow({ height: option.height + WindowConfig.detachWindowTitleHeight, width: option.width, autoHideMenuBar: true, titleBarStyle: "hidden", trafficLightPosition: { x: 10, y: 11 }, title: plugin.title, resizable: true, frame: false, show: false, transparent: false, enableLargerThanScreen: true, backgroundColor: "#fff", alwaysOnTop, x, y, center: true, webPreferences: { webSecurity: false, allowRunningInsecureContent: true, backgroundThrottling: false, nodeIntegration: true, contextIsolation: false, webviewTag: true, devTools: true, navigateOnDragDrop: true, spellcheck: false, preload: preloadDefault, }, }); win._name = `DetachWindow.${view._plugin.name}`; win._plugin = view._plugin; win._type = option.option.type; view._window = win; remoteMain.enable(win.webContents); win.on("close", () => { executePluginHooks(view, "PluginExit", null); removeBrowserViews(view); removeDetachWindows(win); }); win.on("closed", async () => { // @ts-ignore view.webContents?.destroy(); win = undefined; await executeHooks(AppRuntime.mainWindow, "DetachWindowClosed", {}); }); win.on("focus", () => { view && win.webContents?.focus(); }); DevToolsManager.register(`DetachWindow.${view._plugin.name}`, win); win.on("maximize", () => { executeHooks(win, "Maximize"); const display = screen.getDisplayMatching(win.getBounds()); view.setBounds({ x: 0, y: WindowConfig.detachWindowTitleHeight, width: display.workArea.width, height: display.workArea.height - WindowConfig.detachWindowTitleHeight, }); }); win.on("unmaximize", () => { executeHooks(win, "Unmaximize"); const bounds = win.getBounds(); const display = screen.getDisplayMatching(bounds); const width = (display.scaleFactor * bounds.width) % 1 == 0 ? bounds.width : bounds.width - 2; const height = (display.scaleFactor * bounds.height) % 1 == 0 ? bounds.height : bounds.height - 2; view.setBounds({ x: 0, y: WindowConfig.detachWindowTitleHeight, width, height: height - WindowConfig.detachWindowTitleHeight, }); }); win.webContents.once("render-process-gone", () => { // console.log('detach.render-process-gone') win.close(); }); win.webContents.on("before-input-event", (event, input) => { if (input.type === "keyDown") { checkForHotkey(view as any, input); } }); if (isMac) { win.on("enter-full-screen", () => { executeHooks(win, "EnterFullScreen"); }); win.on("leave-full-screen", () => { executeHooks(win, "LeaveFullScreen"); }); } win.webContents.on("will-navigate", (event) => { event.preventDefault(); }); win.webContents.setWindowOpenHandler(() => { return { action: "deny" }; }); if (option.loadUrl) { option.loadUrl(); } const pluginJson = JSON.parse(JSON.stringify(view._plugin)); return new Promise((resolve, reject) => { win.webContents.once("dom-ready", async () => { await executeDarkMode(win, { plugin, isSystem: true, }); view.setAutoResize({ width: true, height: true }); win.setBrowserView(view); view.setBounds({ x: 0, y: WindowConfig.detachWindowTitleHeight, width: option.width, height: option.height, }); DevToolsManager.autoShow(win); const pluginParam = { alwaysOnTop, }; await executeHooks(win, "PluginInit", { plugin: pluginJson, state: option.pluginState, param: pluginParam, }); if ( option.option.type === "action" || (option.option.type === "callPage" && option.option.callPage?.option.showWindow) ) { win.show(); } resolve(undefined); }); rendererLoadPath(win, "page/detachWindow.html"); addDetachWindows(win); }); }, async detach(option?: {}) { if (!mainWindowView) { throw "MainViewNotFound"; } const pluginState: PluginState = await executeHooks( AppRuntime.mainWindow, "PluginState", ); AppRuntime.mainWindow.removeBrowserView(mainWindowView); const bounds = mainWindowView.getBounds(); await this._showInDetachWindow(mainWindowView, { pluginState, width: bounds.width, height: bounds.height, option: { type: "action", }, }); mainWindowView = null; await executeHooks(AppRuntime.mainWindow, "PluginDetached"); AppRuntime.mainWindow.hide(); }, async toggleDetachPluginAlwaysOnTop( view: BrowserView, alwaysOnTop: boolean, option?: {}, ) { view._window.setAlwaysOnTop(alwaysOnTop); return alwaysOnTop; }, async setDetachPluginZoom(view: BrowserView, zoom: number, option?: {}) { view.webContents.setZoomFactor(zoom / 100); }, async firePluginMoreMenuClick( view: BrowserView, name: string, option?: {}, ) { await executePluginHooks(view, "MoreMenuClick", { name }); }, async fireDetachOperateClick(view: BrowserView, name: string, option?: {}) { await executePluginHooks(view, "DetachOperateClick", { name }); }, async closeDetachPlugin(view: BrowserView, option?: {}) { view._window.close(); }, async openDetachPluginDevTools(view: BrowserView, option?: {}) { const devToolsWin = DevToolsManager.getWindow(view); if (devToolsWin) { devToolsWin.close(); } else if (view.webContents.isDevToolsOpened()) { view.webContents.closeDevTools(); } else { view.webContents.openDevTools({ mode: "detach", activate: true, title: `DetachView.${view._plugin.name}`, }); } }, }; ================================================ FILE: electron/mapi/manager/window/remoteWeb.ts ================================================ import { PluginRecord } from "../../../../src/types/Manager"; import { ManagerPlugin } from "../plugin"; import path from "node:path"; import { Files } from "../../file/main"; import { FileUtil } from "../../../lib/util"; import { PluginLog } from "../plugin/log"; type FileMeta = { mimeType: string; headers: Record; }; export const RemoteWebManager = { create: async (plugin: PluginRecord) => { const shouldBlock = (url: string) => { if (plugin.runtime.remoteWeb && plugin.runtime.remoteWeb.blocks) { for (const block of plugin.runtime.remoteWeb.blocks) { if (block.startsWith("/") && block.endsWith("/")) { const regex = new RegExp(block.slice(1, -1)); if (regex.test(url)) { return true; } } else { if (url.includes(block)) { return true; } } } return false; } }; const getFileMeta = async (file: string): Promise => { const defaultMeta: FileMeta = { mimeType: "application/octet-stream", headers: {}, }; const meta = file + ".meta.json"; if (!(await Files.exists(meta, { isDataPath: false }))) { return defaultMeta; } const content = await Files.read(meta, { isDataPath: false }); if (!content) { return defaultMeta; } try { const json: FileMeta = JSON.parse(content); if (json) { return { mimeType: json.mimeType || defaultMeta.mimeType, headers: json.headers || defaultMeta.headers, }; } } catch (e) {} return defaultMeta; }; const writeFileMeta = async ( file: string, meta: FileMeta, ): Promise => { const metaFile = file + ".meta.json"; const content = JSON.stringify(meta, null, 2); await Files.write(metaFile, content, { isDataPath: false }); }; const getCacheFile = (url: string, param: any = {}): string | null => { if (!plugin.runtime.remoteWeb) { return null; } const root = path.join(plugin.runtime.root, "RemoteWebCache"); if (plugin.runtime.remoteWeb.urlMap) { if (plugin.runtime.remoteWeb.urlMap[url]) { return path.join( root, plugin.runtime.remoteWeb.urlMap[url], ); } } if ( !plugin.runtime.remoteWeb.types || !plugin.runtime.remoteWeb.domains ) { return null; } if ( !plugin.runtime.remoteWeb.types.length || !plugin.runtime.remoteWeb.domains.length ) { return null; } const urlInfo = new URL(url); let ext = Files.ext(urlInfo.pathname); if (!ext) { return null; } if (!plugin.runtime.remoteWeb.types.includes(ext)) { return null; } if (!plugin.runtime.remoteWeb.domains.includes(urlInfo.hostname)) { return null; } let f = `${urlInfo.hostname}${urlInfo.pathname}`.replace( /[^a-zA-Z0-9\\/.]/g, "_", ); if (urlInfo.search) { f = f + "-" + urlInfo.search.replace(/[^a-zA-Z0-9\\/.]/g, "_") + "." + ext; } return path.join(root, f); }; const webSession = await ManagerPlugin.getViewSession( plugin, "RemoteWeb", ); if (!webSession.protocol.isProtocolHandled("https")) { const requestHandler = async (request): Promise => { const url = request.url; const file = getCacheFile(url); if (file && (await Files.exists(file, { isDataPath: false }))) { const buffer = await Files.readBuffer(file, { isDataPath: false, }); const fileMeta = await getFileMeta(file); PluginLog.info(plugin.name, "RemoteWeb.Cache.Hit", { url, }); return new Response(buffer, { status: 200, headers: { "content-type": fileMeta.mimeType, "focusany-cache": "hit", ...fileMeta.headers, }, }); } if (!file && shouldBlock(url)) { PluginLog.info(plugin.name, "RemoteWeb.Cache.Blocked", { url, }); return new Response(`RemoteWebBlock - ${url}`, { status: 403, headers: { "content-type": "text/plain" }, }); } return new Promise((resolve, reject) => { fetch(url, { method: request.method || "GET", headers: { ...request.headers, "User-Agent": plugin.runtime.remoteWeb?.userAgent || "FocusAny RemoteWeb Manager", }, }) .then(async (response) => { if (!response.ok) { PluginLog.error( plugin.name, "RemoteWeb.Cache.FetchFailed", { url, status: response.status, statusText: response.statusText, }, ); return resolve( new Response("Fetch failed: " + url, { status: response.status, headers: { "content-type": "text/plain", }, }), ); } const buffer = await response.arrayBuffer(); const mimeType = response.headers.get("content-type") || FileUtil.getMimeByPath( file, "application/octet-stream", ); const headers = {}; response.headers.forEach((value, key) => { headers[key] = value; }); const headerToDelete = [ "content-security-policy", "content-encoding", ]; for (const key of headerToDelete) { if (headers[key]) { delete headers[key]; } } let cacheStatus = "miss"; if (file) { await Files.writeBuffer( file, Buffer.from(buffer), { isDataPath: false }, ); await writeFileMeta(file, { mimeType, headers, }); cacheStatus = "cached"; } PluginLog.info( plugin.name, "RemoteWeb.Cache.Write", { url, mimeType, headers, cacheStatus, length: buffer.byteLength, }, ); resolve( new Response(buffer, { status: 200, headers: { "content-type": mimeType, "focusany-cache": cacheStatus, ...headers, }, }), ); }) .catch((err) => { PluginLog.info( plugin.name, "RemoteWeb.Cache.FetchError", { url, error: err }, ); resolve( new Response( "Fetch error: " + url + ", " + err.message, { status: 500, headers: { "content-type": "text/plain", }, }, ), ); }); }); }; webSession.protocol.handle("https", requestHandler); webSession.protocol.handle("http", requestHandler); } }, }; ================================================ FILE: electron/mapi/misc/index.ts ================================================ import archiver from "archiver"; import axios from "axios"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import yauzl from "yauzl"; const getZipFileContent = async (path: string, pathInZip: string) => { return new Promise((resolve, reject) => { // console.log('getZipFileContent', path, pathInZip) yauzl.open(path, { lazyEntries: true }, (err: any, zipfile: any) => { if (err) { // console.log('getZipFileContent err', err) reject(err); return; } zipfile.on("error", function (err: any) { // console.log('getZipFileContent error', err) reject(err); }); zipfile.on("end", function () { // console.log('getZipFileContent end') reject("FileNotFound"); }); zipfile.on("entry", function (entry: any) { // console.log('getZipFileContent entry', entry.fileName) if (entry.fileName === pathInZip) { zipfile.openReadStream( entry, function (err: any, readStream: any) { if (err) { reject(err); return; } let chunks: any[] = []; readStream.on("data", function (chunk: any) { chunks.push(chunk); }); readStream.on("end", function () { const bytes = Buffer.concat(chunks); const text = bytes.toString("utf8"); resolve(text); }); }, ); } else { zipfile.readEntry(); } }); zipfile.readEntry(); }); }); }; const unzip = async ( zipPath: string, dest: string, option?: { process: (type: "start" | "end", entry: any) => void; }, ) => { option = Object.assign( { process: null, }, option, ); if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } return new Promise((resolve, reject) => { // console.log('unzip', zipPath, dest) yauzl.open(zipPath, { lazyEntries: true }, (err: any, zipfile: any) => { if (err) { // console.log('unzip err', err) reject(err); return; } zipfile.on("error", function (err: any) { // console.log('unzip error', err) reject(err); }); zipfile.on("end", function () { // console.log('unzip end') resolve(undefined); }); zipfile.on("entry", function (entry: any) { if (option.process) { option.process("start", entry); } // console.log('unzip entry', dest, entry.fileName) const destPath = dest + "/" + entry.fileName; if (/\/$/.test(entry.fileName)) { // console.log('unzip mkdir', destPath) fs.mkdirSync(destPath, { recursive: true }); zipfile.readEntry(); } else { const dirname = destPath.replace(/\/[^/]+$/, ""); if (!fs.existsSync(dirname)) { fs.mkdirSync(dirname, { recursive: true }); } zipfile.openReadStream( entry, function (err: any, readStream: any) { if (err) { reject(err); return; } readStream.on("end", function () { if (option.process) { option.process("end", entry); } zipfile.readEntry(); }); readStream.pipe(fs.createWriteStream(destPath)); }, ); } }); zipfile.readEntry(); }); }); }; const zip = async ( zipPath: string, sourceDir: string, option?: { end?: (archive: any) => Promise; filter?: (params: { name: string; path: string; fullPath: string; isDir: boolean; }) => Promise; }, ): Promise => { option = Object.assign( { end: null, filter: null, }, option, ); return new Promise((resolve, reject) => { const output = fs.createWriteStream(zipPath); const archive = archiver("zip", { zlib: { level: 9 }, }); output.on("close", function () { resolve(undefined); }); archive.on("error", function (err: any) { reject(err); }); archive.pipe(output); const addFiles = async (dir: string, relativePath: string = "") => { const items = fs.readdirSync(path.join(dir, relativePath)); for (const item of items) { const fullPath = path.join(dir, relativePath, item); const relPath = path .join(relativePath, item) .replace(/\\/g, "/"); // Normalize for zip const stat = fs.statSync(fullPath); const isDir = stat.isDirectory(); const shouldInclude = !option.filter || (await option.filter({ name: item, path: relPath, fullPath, isDir, })); if (isDir) { if (shouldInclude) { await addFiles(dir, relPath); } } else { if (shouldInclude) { archive.file(fullPath, { name: relPath }); } } } }; addFiles(sourceDir) .then(async () => { if (option.end) { await option.end(archive); } archive.finalize(); }) .catch(reject); }); }; const request = async (option: { url: string; method?: "GET" | "POST"; responseType?: "json" | "text" | "arraybuffer"; headers?: any; data?: any; }) => { option = Object.assign( { url: "", method: "GET", responseType: "json", headers: {}, data: null, }, option, ); const response = await axios.request({ url: option.url, method: option.method, responseType: option.responseType === "arraybuffer" ? "arraybuffer" : "text", headers: option.headers, data: option.data, }); if (response.status !== 200) { throw new Error(`Request failed with status code ${response.status}`); } if (option.responseType === "json") { return JSON.parse(response.data); } else if (option.responseType === "text") { return response.data; } else if (option.responseType === "arraybuffer") { return Buffer.from(response.data); } else { return response.data; } }; const getNetworkInterfaces = () => { const interfaces = os.networkInterfaces(); const result: Array<{ name: string; address: string; family: string; internal: boolean; }> = []; for (const [name, addresses] of Object.entries(interfaces)) { if (!addresses) continue; for (const addr of addresses) { // Filter out internal (loopback) addresses and only include IPv4 if (!addr.internal && addr.family === "IPv4") { result.push({ name, address: addr.address, family: addr.family, internal: addr.internal, }); } } } return result; }; export const Misc = { getZipFileContent, unzip, zip, request, getNetworkInterfaces, }; export default Misc; ================================================ FILE: electron/mapi/misc/main.ts ================================================ import { ipcMain } from "electron"; import index from "./index"; ipcMain.handle( "misc:getZipFileContent", async (_, path: string, pathInZip: string) => { return await index.getZipFileContent(path, pathInZip); }, ); ipcMain.handle("misc:unzip", async (_, zipPath: string, dest: string) => { return await index.unzip(zipPath, dest); }); export default { ...index, }; export const MiscMain = { ...index, }; ================================================ FILE: electron/mapi/misc/render.ts ================================================ import index from "./index"; export default { ...index, }; ================================================ FILE: electron/mapi/protocol/main.ts ================================================ import { Log } from "../log/main"; export const ProtocolMain = { isReady: false, ready() { this.isReady = true; }, url: null, async queue(url: string) { this.url = url; await this.runProtocol(); }, async runProtocol() { return new Promise(async (resolve) => { const run = async () => { if (!this.isReady) { setTimeout(run, 100); return; } if (!this.url) { Log.info( "ProtocolMain.runProtocol.url.Empty", this.filePath, ); return; } const url = this.url; const urlInfo = new URL(url); const command = urlInfo.hostname; const param = urlInfo.searchParams; Log.info("ProtocolMain.runProtocol", { command, param, url, urlInfo, }); if (!command) { Log.info("ProtocolMain.runProtocol.command.Empty", url); return; } if (!this.commandListeners[command]) { Log.info( "ProtocolMain.runProtocol.command.NotFound", command, ); return; } for (const callback of this.commandListeners[command]) { callback(Object.fromEntries(param.entries())); } resolve(undefined); }; run().then(); }); }, commandListeners: {} as { [command: string]: Array<(params: { [key: string]: string }) => void>; }, register( command: string, callback: (params: { [key: string]: string }) => void, ) { if (!this.commandListeners[command]) { this.commandListeners[command] = []; } this.commandListeners[command].push(callback); }, unregister( command: string, callback: (params: { [key: string]: string }) => void, ) { if (!this.commandListeners[command]) { return; } const index = this.commandListeners[command].indexOf(callback); if (index >= 0) { this.commandListeners[command].splice(index, 1); } }, }; export default ProtocolMain; ================================================ FILE: electron/mapi/render.ts ================================================ import { exposeContext } from "./util"; import { AppEnv } from "./env"; import config from "./config/render"; import log from "./log/render"; import app from "./app/render"; import storage from "./storage/render"; import db from "./db/render"; import file from "./file/render"; import event from "./event/render"; import ui from "./ui/render"; import updater from "./updater/render"; import statistics from "./statistics/render"; import user from "./user/render"; import misc from "./misc/render"; import manager from "./manager/render"; import kvdb from "./kvdb/render"; export const MAPI = { init(env: typeof AppEnv = null) { if (!env) { // expose context exposeContext("$mapi", { app, log, config, storage, db, file, event, ui, updater, statistics, user, misc, manager, kvdb, }); db.init(); event.init(); ui.init(); } else { // init context AppEnv.appRoot = env.appRoot; AppEnv.appData = env.appData; AppEnv.userData = env.userData; AppEnv.dataRoot = env.dataRoot; AppEnv.isInit = true; } }, }; ================================================ FILE: electron/mapi/statistics/render.ts ================================================ import { AppConfig } from "../../../src/config"; import { memoryInfo, platformArch, platformName, platformUUID, platformVersion, } from "../../lib/env"; import { post } from "../../lib/api"; let tickDataList = []; let tickSendTimer = null; const tickSendAsync = () => { if (tickSendTimer) { clearTimeout(tickSendTimer); tickSendTimer = null; } if (!AppConfig.statisticsUrl) { tickDataList = []; return; } tickSendTimer = setTimeout(async () => { tickSendTimer = null; if (!tickDataList.length) { return; } // console.log('tickSend', JSON.stringify(tickDataList)) post(AppConfig.statisticsUrl, { data: tickDataList, version: AppConfig.version, uuid: platformUUID(), platform: { name: platformName(), version: platformVersion(), arch: platformArch(), mem: memoryInfo(), }, }) .then((res) => { // console.log('tickSend', tickDataList, res) }) .catch((err) => { // console.error('tickSend', tickDataList, err) }); tickDataList = []; }, 2000); }; const tick = (name: string, data: any) => { tickDataList.push({ name, data, }); tickSendAsync(); }; export default { tick, }; ================================================ FILE: electron/mapi/storage/main.ts ================================================ import { AppEnv, waitAppEnvReady } from "../env"; import fs from "node:fs"; import { ipcMain } from "electron"; import nodePath from "node:path"; let data = {}; const userDataRoot = () => { return nodePath.join(AppEnv.userData, "storage"); }; const dataRoot = () => { return nodePath.join(AppEnv.dataRoot, "storage"); }; const filePath = (group: string) => { let p = nodePath.join(userDataRoot(), `${group}.json`); if (fs.existsSync(p)) { return p; } return nodePath.join(dataRoot(), `${group}.json`); }; const load = (group: string) => { try { const p = filePath(group); let json = fs.readFileSync(p).toString(); json = JSON.parse(json); data[group] = json || {}; } catch (e) { data[group] = {}; } }; const loadIfNeed = (group: string) => { if (!(group in data)) { load(group); } }; const save = (group: string) => { const path = filePath(group); const dir = nodePath.dirname(path); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(path, JSON.stringify(data[group], null, 4)); }; const all = async (group: string) => { await waitAppEnvReady(); loadIfNeed(group); return data[group]; }; const get = async (group: string, key: string, defaultValue: any) => { await waitAppEnvReady(); loadIfNeed(group); if (!(key in data[group])) { data[group][key] = defaultValue; save(group); } return data[group][key]; }; const set = async (group: string, key: string, value: any) => { await waitAppEnvReady(); loadIfNeed(group); data[group][key] = value; save(group); }; const read = async (group: string, defaultValue: any) => { await waitAppEnvReady(); loadIfNeed(group); if (!(group in data)) { data[group] = defaultValue; save(group); } return data[group]; }; const write = async (group: string, value: any) => { await waitAppEnvReady(); loadIfNeed(group); data[group] = value; save(group); }; ipcMain.handle("storage:all", async (event, group: string) => { return await all(group); }); ipcMain.handle( "storage:get", async (event, group: string, key: string, defaultValue: any) => { return await get(group, key, defaultValue); }, ); ipcMain.handle( "storage:set", async (event, group: string, key: string, value: any) => { return await set(group, key, value); }, ); ipcMain.handle( "storage:read", async (event, group: string, defaultValue: any) => { return await read(group, defaultValue); }, ); ipcMain.handle("storage:write", async (event, group: string, value: any) => { return await write(group, value); }); export const StorageMain = { all, get, set, read, write, }; export default StorageMain; ================================================ FILE: electron/mapi/storage/render.ts ================================================ import { ipcRenderer } from "electron"; const all = async (group: string) => { return ipcRenderer.invoke("storage:all", group); }; const get = async (group: string, key: string, defaultValue: any) => { return ipcRenderer.invoke( "storage:get", group, key, JSON.parse(JSON.stringify(defaultValue)), ); }; const set = async (group: string, key: string, value: any) => { return ipcRenderer.invoke( "storage:set", group, key, JSON.parse(JSON.stringify(value)), ); }; const read = async (group: string, defaultValue: any = null) => { return ipcRenderer.invoke( "storage:read", group, JSON.parse(JSON.stringify(defaultValue)), ); }; const write = async (group: string, value: any) => { return ipcRenderer.invoke( "storage:write", group, JSON.parse(JSON.stringify(value)), ); }; export default { all, get, set, read, write, }; ================================================ FILE: electron/mapi/ui/index.ts ================================================ export default {}; ================================================ FILE: electron/mapi/ui/render.ts ================================================ const init = () => { // initLoaders() }; const initLoaders = () => { function domReady( condition: DocumentReadyState[] = ["complete", "interactive"], ) { return new Promise((resolve) => { if (condition.includes(document.readyState)) { resolve(true); } else { document.addEventListener("readystatechange", () => { if (condition.includes(document.readyState)) { resolve(true); } }); } }); } const safeDOM = { append(parent: HTMLElement, child: HTMLElement) { if (!Array.from(parent.children).find((e) => e === child)) { return parent.appendChild(child); } }, remove(parent: HTMLElement, child: HTMLElement) { if (Array.from(parent.children).find((e) => e === child)) { return parent.removeChild(child); } }, }; /** * https://tobiasahlin.com/spinkit * https://connoratherton.com/loaders * https://projects.lukehaas.me/css-loaders * https://matejkustec.github.io/SpinThatShit */ function useLoading() { const className = `loaders-css__square-spin`; const styleContent = ` @keyframes loading-spin { 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0% ,calc(100%/3) 100%} 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0% } } .${className} > div { width: 60px; aspect-ratio: 4; --_g: no-repeat radial-gradient(circle closest-side,#cbd5e1 90%,#cbd5e100); background: var(--_g) 0% 50%, var(--_g) 50% 50%, var(--_g) 100% 50%; background-size: calc(100%/3) 100%; animation: loading-spin 1s infinite linear; } .app-loading-wrap { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background: #FFFFFF; z-index: 10000; } [data-theme="dark"] .app-loading-wrap { background: #17171A; } [data-theme="dark"] .${className} > div { --_g: no-repeat radial-gradient(circle closest-side,#2D3748 90%,#2D374800); } `; const oStyle = document.createElement("style"); const oDiv = document.createElement("div"); let hasLoading = false; let setLoadingTimer = null; oStyle.id = "app-loading-style"; oStyle.innerHTML = styleContent; oDiv.className = "app-loading-wrap"; oDiv.innerHTML = `
`; return { appendLoading() { setLoadingTimer = setTimeout(() => { safeDOM.append(document.head, oStyle); safeDOM.append(document.body, oDiv); hasLoading = true; }, 1000); }, removeLoading() { clearTimeout(setLoadingTimer); if (hasLoading) { safeDOM.remove(document.head, oStyle); safeDOM.remove(document.body, oDiv); hasLoading = false; } }, }; } const { appendLoading, removeLoading } = useLoading(); const isMain = () => { return true; let l = window.location.href; if (l.indexOf("app.asar/dist/index.html") > 0) { return true; } if (l.indexOf("localhost") > 0 && l.indexOf(".html") === -1) { return true; } return false; }; if (isMain()) { domReady().then(appendLoading); window.onmessage = (ev) => { ev.data.payload === "removeLoading" && removeLoading(); }; } setTimeout(removeLoading, 4999); }; export default { init, }; ================================================ FILE: electron/mapi/updater/index.ts ================================================ import { AppConfig } from "../../../src/config"; import { platformArch, platformName, platformUUID, platformVersion, } from "../../lib/env"; const checkForUpdate = async () => { try { const res = await fetch(AppConfig.updaterUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ version: AppConfig.version, uuid: platformUUID(), platform: { name: platformName(), version: platformVersion(), arch: platformArch(), }, }), }); return await res.json(); } catch (e) { return { code: -1, msg: `Failed to check update : ${e.message}`, }; } }; export default { checkForUpdate, }; ================================================ FILE: electron/mapi/updater/main.ts ================================================ import updaterIndex from "./index"; import { ipcMain } from "electron"; import ConfigMain from "../config/main"; ipcMain.handle("updater:getCheckAtLaunch", async (event) => { return ConfigMain.get("updaterCheckAtLaunch", "yes"); }); ipcMain.handle("updater:setCheckAtLaunch", async (event, value) => { return ConfigMain.set("updaterCheckAtLaunch", value); }); export const UpdaterMain = { ...updaterIndex, }; export default UpdaterMain; ================================================ FILE: electron/mapi/updater/render.ts ================================================ import updaterIndex from "./index"; import { ipcRenderer } from "electron"; const getCheckAtLaunch = async (): Promise<"yes" | "no"> => { return ipcRenderer.invoke("updater:getCheckAtLaunch"); }; const setCheckAtLaunch = async (value: "yes" | "no"): Promise => { return ipcRenderer.invoke("updater:setCheckAtLaunch", value); }; export default { ...updaterIndex, getCheckAtLaunch, setCheckAtLaunch, }; ================================================ FILE: electron/mapi/user/main.ts ================================================ import { ipcMain, shell } from "electron"; import { AppConfig } from "../../../src/config"; import { ResultType } from "../../lib/api"; import { Events } from "../event/main"; import { platformUUID } from "../../lib/env"; import { AppsMain } from "../app/main"; import Apps from "../app"; import StorageMain from "../storage/main"; import { Log } from "../log/main"; import { ManagerPluginEvent } from "../manager/plugin/event"; const init = async () => { setTimeout(() => { refresh().then(); }, 1000); return null; }; const userData = { isInit: false, apiToken: "", user: { id: "", name: "", avatar: "", deviceCode: "", }, data: {}, basic: {}, }; const get = async (): Promise<{ apiToken: string; user: { id: string; name: string; avatar: string; deviceCode: string; }; data: { [key: string]: any; }; basic: { [key: string]: any; }; }> => { if (!userData.isInit) { const userStorageData = await StorageMain.get("user", "data", {}); userData.apiToken = userStorageData.apiToken || ""; userData.user = userStorageData.user || {}; userData.data = userStorageData.data || {}; userData.basic = userStorageData.basic || {}; userData.isInit = true; } userData.user.id = userData.user.id || ""; return { apiToken: userData.apiToken, user: userData.user, data: userData.data, basic: userData.basic, }; }; ipcMain.handle( "user:open", async ( event, option?: { readyParam: { page?: string; [key: string]: any; }; }, ) => { option = Object.assign( { readyParam: null, }, option || {}, ); await AppsMain.windowOpen("user", option); if (option.readyParam) { await Events.callPage("user", "ready", option.readyParam); } }, ); ipcMain.handle("user:get", async (event) => { return get(); }); const save = async (data: { apiToken: string; user: any; data: any; basic: {}; }) => { const userChanged = JSON.stringify(userData.user) !== JSON.stringify(data.user); userData.apiToken = data.apiToken || ""; userData.user = data.user || {}; userData.data = data.data || {}; userData.user.id = userData.user.id || ""; if (userChanged) { Events.broadcast("UserChange", {}); ManagerPluginEvent.firePluginEvent("UserChange", {}).then(); } await StorageMain.set("user", "data", { apiToken: data.apiToken, user: data.user, data: data.data, basic: data.basic, }); }; ipcMain.handle("user:save", async (event, data) => { return save(data); }); const refresh = async () => { const result = await userInfoApi(); // console.log("user.refresh", JSON.stringify(result, null, 2)); await save({ apiToken: result.data.apiToken, user: result.data.user, data: result.data.data, basic: result.data.basic, }); }; ipcMain.handle("user:refresh", async (event) => { return refresh(); }); const getApiToken = async (): Promise => { await get(); return userData.apiToken; }; ipcMain.handle("user:getApiToken", async (event) => { return getApiToken(); }); const getWebEnterUrl = async (url: string) => { let param = []; const apiToken = await getApiToken(); if (apiToken) { param.push(`api_token=${apiToken}`); } if (await AppsMain.shouldDarkMode()) { param.push(`is_dark=1`); } param.push(`device_uuid=${platformUUID()}`); param.push(`url=${encodeURIComponent(url)}`); return `${AppConfig.apiBaseUrl}/app_manager/enter?${param.join("&")}`; }; ipcMain.handle("user:getWebEnterUrl", async (event, url) => { return getWebEnterUrl(url); }); const openWebUrl = async (url: string) => { url = await getWebEnterUrl(url); await shell.openExternal(url); }; ipcMain.handle("user:openWebUrl", async (event, url) => { return openWebUrl(url); }); const apiPost = async ( url: string, data: Record, option?: { throwException?: boolean; }, ) => { return post(url, data, option); }; ipcMain.handle("user:apiPost", async (event, url, data, option) => { return apiPost(url, data, option); }); export const User = { init, get, save, getApiToken, getWebEnterUrl, openWebUrl, }; export default User; const post = async ( api: string, data: Record, option?: { throwException?: boolean; retry?: number; retryTimes?: number; retryInterval?: number; }, ): Promise> => { option = Object.assign( { throwException: true, retry: 0, retryTimes: 0, retryInterval: 5, }, option, ); let url = api; if (!api.startsWith("http:") && !api.startsWith("https:")) { url = `${AppConfig.apiBaseUrl}/${api}`; } const apiToken = await User.getApiToken(); let json = null, res = null; try { res = await fetch(url, { method: "POST", headers: { "User-Agent": Apps.getUserAgent(), "Content-Type": "application/json", "Api-Token": apiToken, }, body: JSON.stringify(data), }); if (res.status !== 200) { if (option.retry > 0 && option.retryTimes < option.retry) { option.retryTimes++; Log.info("user.post.retry", { api, data, res, retryTimes: option.retryTimes, }); await new Promise((resolve) => setTimeout(resolve, option.retryInterval * 1000), ); return await post(api, data, option); } Log.error("user.post.error", { api, data, res }); if (option.throwException) { throw `RequestError(code:${res.status},text:${res.statusText})`; } return { code: 10000, msg: `RequestError(code:${res.status},text:${res.statusText})`, } as ResultType; } json = await res.json(); } catch (e) { res = `RequestError(${e})`; } // console.log('post', JSON.stringify({api, data, json}, null, 2)) if (!json || !("code" in json)) { if (option.retry > 0 && option.retryTimes < option.retry) { option.retryTimes++; Log.info("user.post.retry", { api, data, res, retryTimes: option.retryTimes, }); await new Promise((resolve) => setTimeout(resolve, option.retryInterval * 1000), ); return await post(api, data, option); } Log.error("user.post.error", { api, data, res }); if (option.throwException) { throw "ResponseError"; } return { code: 10000, msg: "ResponseError" }; } if (json.code) { // login required if (json.code === 1001) { if (userData.user && userData.user.id) { await refresh(); } } if (option.throwException) { throw json.msg; } } return json; }; const userInfoApi = async (): Promise< ResultType<{ apiToken: string; user: object; data: any; basic: object; }> > => { return await post("app_manager/user_info", {}); }; export const UserApi = { post, userInfoApi, }; ================================================ FILE: electron/mapi/user/render.ts ================================================ import { ipcRenderer } from "electron"; const open = async (option: any) => { return ipcRenderer.invoke("user:open", option); }; const get = async (): Promise => { return ipcRenderer.invoke("user:get"); }; const refresh = async () => { return ipcRenderer.invoke("user:refresh"); }; const getApiToken = async (): Promise => { return ipcRenderer.invoke("user:getApiToken"); }; const getWebEnterUrl = async (url: string) => { return ipcRenderer.invoke("user:getWebEnterUrl", url); }; const openWebUrl = async (url: string) => { return ipcRenderer.invoke("user:openWebUrl", url); }; const apiPost = async ( url: string, data: Record, option?: { throwException?: boolean; }, ) => { return ipcRenderer.invoke("user:apiPost", url, data, option); }; export default { open, get, refresh, getApiToken, getWebEnterUrl, openWebUrl, apiPost, }; ================================================ FILE: electron/mapi/util.ts ================================================ import { contextBridge } from "electron"; export function exposeContext(key, value) { if (process.contextIsolated) { try { contextBridge.exposeInMainWorld(key, value); } catch (error) { console.error(error); } } else { window[key] = value; } } ================================================ FILE: electron/page/about.ts ================================================ import { BrowserWindow } from "electron"; import { t } from "../config/lang"; import { WindowConfig } from "../config/window"; import { preloadDefault } from "../lib/env-main"; import { Page } from "./index"; export const PageAbout = { NAME: "about", open: async (option: any) => { const win = new BrowserWindow({ title: t("page.about.title"), parent: null, minWidth: WindowConfig.aboutWidth, minHeight: WindowConfig.aboutHeight, width: WindowConfig.aboutWidth, height: WindowConfig.aboutHeight, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, }, show: true, frame: false, transparent: false, }); return Page.openWindow(PageAbout.NAME, win, "page/about.html"); }, }; ================================================ FILE: electron/page/feedback.ts ================================================ import { BrowserWindow } from "electron"; import { t } from "../config/lang"; import { WindowConfig } from "../config/window"; import { preloadDefault } from "../lib/env-main"; import { Page } from "./index"; export const PageFeedback = { NAME: "feedback", open: async (option: any) => { const win = new BrowserWindow({ title: t("page.feedback.title"), parent: null, minWidth: WindowConfig.feedbackWidth, minHeight: WindowConfig.feedbackHeight, width: WindowConfig.feedbackWidth, height: WindowConfig.feedbackHeight, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, }, show: true, frame: false, transparent: false, }); return Page.openWindow(PageFeedback.NAME, win, "page/feedback.html"); }, }; ================================================ FILE: electron/page/guide.ts ================================================ import { BrowserWindow } from "electron"; import { preloadDefault, rendererLoadPath } from "../lib/env-main"; import { Page } from "./index"; import { AppConfig } from "../../src/config"; import { icnsLogoPath, icoLogoPath, logoPath } from "../config/icon"; import { isPackaged } from "../lib/env"; import { WindowConfig } from "../config/window"; import * as remoteMain from "@electron/remote/main"; import { DevToolsManager } from "../lib/devtools"; export const PageGuide = { NAME: "guide", open: async (option: any) => { let icon = logoPath; if (process.platform === "win32") { icon = icoLogoPath; } else if (process.platform === "darwin") { icon = icnsLogoPath; } const win = new BrowserWindow({ show: true, title: AppConfig.title, ...(!isPackaged ? { icon } : {}), frame: false, transparent: false, hasShadow: true, center: true, useContentSize: true, minWidth: WindowConfig.guideWidth, minHeight: WindowConfig.guideHeight, width: WindowConfig.guideWidth, height: WindowConfig.guideHeight, skipTaskbar: true, resizable: false, maximizable: false, backgroundColor: "#f1f5f9", alwaysOnTop: false, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, }, }); win.on("closed", () => { Page.unregisterWindow(PageGuide.NAME); }); rendererLoadPath(win, "page/guide.html"); remoteMain.enable(win.webContents); win.webContents.on("did-finish-load", () => { Page.ready("guide"); DevToolsManager.autoShow(win); }); DevToolsManager.register("Guide", win); // win.webContents.setWindowOpenHandler(({url}) => { // if (url.startsWith('https:')) shell.openExternal(url) // return {action: 'deny'} // }) Page.registerWindow(PageGuide.NAME, win); }, }; ================================================ FILE: electron/page/index.ts ================================================ import { Events } from "../mapi/event/main"; import { AppEnv, AppRuntime } from "../mapi/env"; import { PageUser } from "./user"; import { BrowserWindow, shell } from "electron"; import { rendererLoadPath } from "../lib/env-main"; import { PageGuide } from "./guide"; import { PageSetup } from "./setup"; import { PageAbout } from "./about"; import { DevToolsManager } from "../lib/devtools"; import { PageFeedback } from "./feedback"; import { PagePayment } from "./payment"; import { PageMonitor } from "./monitor"; import { PageLog } from "./log"; const Pages = { user: PageUser, guide: PageGuide, setup: PageSetup, payment: PagePayment, about: PageAbout, feedback: PageFeedback, monitor: PageMonitor, log: PageLog, }; export const Page = { ready(name: string) { Events.send(name, "APP_READY", { name, AppEnv, }); }, openWindow: (name: string, win: BrowserWindow, fileName: string) => { win.webContents.on("will-navigate", (event) => { event.preventDefault(); }); win.webContents.setWindowOpenHandler(() => { return { action: "deny" }; }); win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith("https:") || url.startsWith("http:")) { shell.openExternal(url).then(); } return { action: "deny" }; }); win.on("close", () => { delete AppRuntime.windows[name]; }); const promise = new Promise((resolve, reject) => { win.webContents.on("did-finish-load", () => { win.focus(); Page.ready(name); DevToolsManager.autoShow(win); resolve(undefined); }); }); rendererLoadPath(win, fileName); DevToolsManager.register(`Page.${name}`, win); AppRuntime.windows[name] = win; return promise; }, open: async ( name: string, option?: { singleton?: boolean; parent?: BrowserWindow; [key: string]: any; }, ) => { option = Object.assign( { singleton: true, parent: null, }, option, ); if (!option.parent) { option.parent = AppRuntime.mainWindow; } if (option.singleton && AppRuntime.windows[name]) { AppRuntime.windows[name].show(); AppRuntime.windows[name].focus(); AppRuntime.windows[name].setParentWindow(option.parent); return; } return Pages[name].open(option); }, registerWindow(name: string, win: BrowserWindow) { AppRuntime.windows[name] = win; }, unregisterWindow(name: string) { delete AppRuntime.windows[name]; }, }; ================================================ FILE: electron/page/log.ts ================================================ import { BrowserWindow } from "electron"; import { t } from "../config/lang"; import { WindowConfig } from "../config/window"; import { preloadDefault } from "../lib/env-main"; import { AppRuntime } from "../mapi/env"; import { Page } from "./index"; export const PageLog = { NAME: "log", open: async (option: { log: string }) => { if (AppRuntime.windows[PageLog.NAME]) { AppRuntime.windows[PageLog.NAME].close(); } const win = new BrowserWindow({ title: t("page.log.title"), parent: null, minWidth: WindowConfig.logWidth, minHeight: WindowConfig.logHeight, width: WindowConfig.logWidth, height: WindowConfig.logHeight, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, }, show: true, frame: false, transparent: false, }); await Page.openWindow(PageLog.NAME, win, "page/log.html"); const logInit = { log: option.log, }; win.webContents.executeJavaScript(` const logInit = ()=>{ if(!window.__logInit){ setTimeout(logInit, 100); return; } window.__logInit(${JSON.stringify(logInit)}); };logInit(); `); }, }; ================================================ FILE: electron/page/monitor.ts ================================================ import { BrowserWindow } from "electron"; import { t } from "../config/lang"; import { preloadDefault } from "../lib/env-main"; import { Events } from "../mapi/event/main"; import { Page } from "./index"; export const PageMonitor = { NAME: "monitor", open: async (option: { title?: string; width?: number; height?: number; [key: string]: any; }) => { option = Object.assign( { title: t("page.monitor.title"), width: 700, height: 500, url: "", script: null, openDevTools: false, broadcastPages: [], }, option, ); const win = new BrowserWindow({ title: option.title, width: option.width, height: option.height, webPreferences: { nodeIntegration: true, contextIsolation: false, webSecurity: false, preload: preloadDefault, webviewTag: true, }, show: true, frame: false, center: true, transparent: false, focusable: true, parent: null, alwaysOnTop: false, }); const sendMonitorData = async (type: string, data: any) => { return Events.callPage(PageMonitor.NAME, "MonitorData", { type, data, }); }; win.webContents.on("did-finish-load", () => { sendMonitorData("SetTitle", { title: option.title }); sendMonitorData("LoadUrl", { url: option.url, script: option.script, openDevTools: option.openDevTools, }); }); win.webContents.on("ipc-message", (event, channel, ...args) => { if (channel === "MonitorEvent") { const { type, data } = args[0]; // console.log('MonitorEvent', type, data) if (option.broadcastPages.length > 0) { Events.broadcast( "MonitorEvent", { type, data }, { pages: option.broadcastPages, }, ); } } }); await Page.openWindow(PageMonitor.NAME, win, "page/monitor.html"); }, }; ================================================ FILE: electron/page/payment.ts ================================================ import { BrowserWindow, ipcMain } from "electron"; import { preloadDefault, rendererLoadPath } from "../lib/env-main"; import { Page } from "./index"; import { AppConfig } from "../../src/config"; import { icnsLogoPath, icoLogoPath, logoPath } from "../config/icon"; import { isPackaged } from "../lib/env"; import { WindowConfig } from "../config/window"; import * as remoteMain from "@electron/remote/main"; import { DevToolsManager } from "../lib/devtools"; export const PagePayment = { NAME: "payment", event: { onRefresh: null, onWatch: null, onClose: null, }, open: async (option: { onRefresh: () => Promise<{ payUrl: string; watchUrl: string; payExpireSeconds: number; body: string; }>; onWatch: () => Promise<{ status: "WaitPay" | "Scanned" | "Payed" | "Expired" | "Error"; }>; onClose: () => void; parent?: BrowserWindow; }): Promise<{ close: () => void; }> => { PagePayment.event.onRefresh = option.onRefresh; PagePayment.event.onWatch = option.onWatch; PagePayment.event.onClose = option.onClose; let icon = logoPath; if (process.platform === "win32") { icon = icoLogoPath; } else if (process.platform === "darwin") { icon = icnsLogoPath; } let parent = option.parent || null; let alwaysOnTop = !parent; const win = new BrowserWindow({ show: true, title: AppConfig.title, ...(!isPackaged ? { icon } : {}), frame: false, transparent: false, hasShadow: true, center: true, useContentSize: true, minWidth: WindowConfig.paymentWidth, minHeight: WindowConfig.paymentHeight, width: WindowConfig.paymentWidth, height: WindowConfig.paymentHeight, skipTaskbar: true, resizable: false, maximizable: false, backgroundColor: "#f1f5f9", focusable: true, parent, alwaysOnTop, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, // sandbox: false, }, }); win.on("closed", () => { Page.unregisterWindow(PagePayment.NAME); PagePayment.event.onClose(); }); rendererLoadPath(win, "page/payment.html"); remoteMain.enable(win.webContents); win.webContents.on("did-finish-load", () => { Page.ready("payment"); DevToolsManager.autoShow(win); win.focus(); }); DevToolsManager.register("Payment", win); // win.webContents.setWindowOpenHandler(({url}) => { // if (url.startsWith('https:')) shell.openExternal(url) // return {action: 'deny'} // }) Page.registerWindow(PagePayment.NAME, win); return { close: () => { win.close(); }, }; }, }; ipcMain.handle( "Payment.Event", async (event, type: "refresh" | "watch", param: any) => { switch (type) { case "refresh": return await PagePayment.event.onRefresh(); case "watch": return await PagePayment.event.onWatch(); } }, ); ================================================ FILE: electron/page/setup.ts ================================================ import { BrowserWindow } from "electron"; import { preloadDefault, rendererLoadPath } from "../lib/env-main"; import { Page } from "./index"; import { AppConfig } from "../../src/config"; import { icnsLogoPath, icoLogoPath, logoPath } from "../config/icon"; import { isPackaged } from "../lib/env"; import { WindowConfig } from "../config/window"; import * as remoteMain from "@electron/remote/main"; import { DevToolsManager } from "../lib/devtools"; export const PageSetup = { NAME: "setup", open: async (option: any) => { let icon = logoPath; if (process.platform === "win32") { icon = icoLogoPath; } else if (process.platform === "darwin") { icon = icnsLogoPath; } const win = new BrowserWindow({ show: true, title: AppConfig.title, ...(!isPackaged ? { icon } : {}), frame: false, transparent: false, hasShadow: true, center: true, useContentSize: true, minWidth: WindowConfig.guideWidth, minHeight: WindowConfig.guideHeight, width: WindowConfig.guideWidth, height: WindowConfig.guideHeight, skipTaskbar: true, resizable: false, maximizable: false, backgroundColor: "#f1f5f9", alwaysOnTop: true, webPreferences: { preload: preloadDefault, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production nodeIntegration: true, webSecurity: false, webviewTag: true, // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation contextIsolation: false, // sandbox: false, }, }); win.on("closed", () => { Page.unregisterWindow(PageSetup.NAME); }); rendererLoadPath(win, "page/setup.html"); remoteMain.enable(win.webContents); win.webContents.on("did-finish-load", () => { Page.ready("setup"); DevToolsManager.autoShow(win); }); DevToolsManager.register("Setup", win); // win.webContents.setWindowOpenHandler(({url}) => { // if (url.startsWith('https:')) shell.openExternal(url) // return {action: 'deny'} // }) Page.registerWindow(PageSetup.NAME, win); }, }; ================================================ FILE: electron/page/user.ts ================================================ import { BrowserWindow } from "electron"; import { t } from "../config/lang"; import { preloadDefault } from "../lib/env-main"; import { Page } from "./index"; export const PageUser = { NAME: "user", open: async (option: { parent?: BrowserWindow }) => { option = Object.assign( { parent: null, }, option, ); let alwaysOnTop = !option.parent; const win = new BrowserWindow({ title: t("page.user.title"), minWidth: 700, minHeight: 500, width: 700, height: 500, webPreferences: { nodeIntegration: true, contextIsolation: false, webSecurity: false, preload: preloadDefault, webviewTag: true, }, show: true, frame: false, center: true, transparent: false, focusable: true, parent: option.parent, alwaysOnTop, }); return Page.openWindow(PageUser.NAME, win, "page/user.html"); }, }; ================================================ FILE: electron/preload/focusany.ts ================================================ import electronRemote from "@electron/remote"; import { ipcRenderer, shell } from "electron"; import fs from "fs"; import os from "os"; import path from "path"; import zhCN from "../../src/lang/zh-CN.json"; import { isMac } from "../lib/env"; import { EncodeUtil, FileUtil, HotKeyUtil, StrUtil, TimeUtil, } from "../lib/util"; const t = (key: string): string => (zhCN as any)[key] || key; const ipcSendSync = (type: string, data?: any) => { executeHook("Log", `${type}`, data); const result = ipcRenderer.sendSync("FocusAny.Plugin", { type, data, }); executeHook("Log", `${type}.result`, result); if (result instanceof Error) throw result; return result; }; const ipcSendAsync = async (type: string, data?: any) => { executeHook("Log", `${type}`, data); const result = await ipcRenderer.invoke("FocusAny.Plugin.Async", { type, data, }); executeHook("Log", `${type}.result`, result); if (result instanceof Error) throw result; return result; }; const ipcSend = (type: string, data?: any) => { ipcRenderer.send("FocusAny.Plugin", { type, data, }); executeHook("Log", `${type}`, data); }; const ipcSendToHost = ( type: string, data?: any, hasResult?: boolean, ): Promise => { hasResult = hasResult || false; const id = StrUtil.randomString(16); return new Promise((resolve, reject) => { if (hasResult) { const timeoutTimer = setTimeout(() => { executeHook("Log", `${type}.timeout`); ipcRenderer.removeAllListeners(`FocusAny.View.${id}`); reject(new Error("timeout")); }, 60 * 1000); ipcRenderer.once(`FocusAny.View.${id}`, (_event, result) => { executeHook("Log", `${type}.result`, result); clearTimeout(timeoutTimer); resolve(result); }); } ipcRenderer.sendToHost("FocusAny.View", { id, type, data, }); executeHook("Log", `${type}`, data); if (!hasResult) { resolve(null); } }); }; const executeHook = (hook: string, ...data: any[]) => { hook = `on${hook}`; if (FocusAny.hooks[hook]) { FocusAny.hooks[hook](...data); } }; export const FocusAny = { hooks: {} as any, onPluginReady(cb: Function) { FocusAny.hooks.onPluginReady = cb; }, onPluginExit(cb: Function) { FocusAny.hooks.onPluginExit = cb; }, onPluginEvent(event: PluginEvent, callback: (data: any) => void) { if (!("onPluginEvent" in FocusAny.hooks)) { FocusAny.hooks.onPluginEvent = (payload: { event: string; data: any; }) => { const { event, data } = payload; if (event in FocusAny.hooks.onPluginEventCallbacks) { FocusAny.hooks.onPluginEventCallbacks[event].forEach( (cb: (data: any) => void) => { cb(data); }, ); } }; } if (!("onPluginEventCallbacks" in FocusAny.hooks)) { FocusAny.hooks.onPluginEventCallbacks = {}; } if (!(event in FocusAny.hooks.onPluginEventCallbacks)) { FocusAny.hooks.onPluginEventCallbacks[event] = []; } FocusAny.hooks.onPluginEventCallbacks[event].push(callback); ipcSend("registerPluginEvent", { event }); }, offPluginEvent(event: PluginEvent, callback: (data: any) => void) { if (!("onPluginEventCallbacks" in FocusAny.hooks)) { FocusAny.hooks.onPluginEventCallbacks = {}; } if (!(event in FocusAny.hooks.onPluginEventCallbacks)) { return; } FocusAny.hooks.onPluginEventCallbacks[event] = FocusAny.hooks.onPluginEventCallbacks[event].filter( (c) => c !== callback, ); if (FocusAny.hooks.onPluginEventCallbacks[event].length === 0) { delete FocusAny.hooks.onPluginEventCallbacks[event]; ipcSend("unregisterPluginEvent", { event }); } }, offPluginEventAll(event: PluginEvent) { if (!("onPluginEventCallbacks" in FocusAny.hooks)) { FocusAny.hooks.onPluginEventCallbacks = {}; } if (!(event in FocusAny.hooks.onPluginEventCallbacks)) { return; } delete FocusAny.hooks.onPluginEventCallbacks[event]; ipcSend("unregisterPluginEvent", { event }); }, onMoreMenuClick(callback: (data: { name: string }) => void) { FocusAny.hooks.onMoreMenuClick = callback; }, registerHotkey( key: string | string[] | HotkeyQuickType | HotkeyType | HotkeyType[], callback: () => void, ) { if ("save" === key) { if (isMac) { key = "Command+S"; } else { key = "Ctrl+S"; } } const hotkeys = HotKeyUtil.unify(key); if (!("hotKeyListeners" in FocusAny.hooks)) { FocusAny.hooks.hotKeyListeners = []; FocusAny.hooks.onHotkey = (payload: { id: string; hotkey: HotkeyType; }) => { const { id, hotkey } = payload; FocusAny.hooks.hotKeyListeners.forEach( (listener: { id: string; hotkeys: HotkeyType[]; callback: () => void; }) => { if (listener.id === id) { listener.callback(); } }, ); }; } const id = StrUtil.randomString(16); FocusAny.hooks.hotKeyListeners.push({ id, hotkeys, callback }); ipcSend("registerHotkey", { id, hotkeys }); }, unregisterHotkeyAll() { FocusAny.hooks.hotKeyListeners = []; ipcSend("unregisterHotkeyAll", {}); }, onLog(cb: Function) { FocusAny.hooks.onLog = cb; }, isMacOs() { return os.type() === "Darwin"; }, isWindows() { return os.type() === "Windows_NT"; }, isLinux() { return os.type() === "Linux"; }, getPlatformArch() { return ipcSendSync("getPlatformArch"); }, isMainWindowShown(): boolean { return ipcSendSync("isMainWindowShown"); }, hideMainWindow() { ipcSend("hideMainWindow", {}); }, showMainWindow() { ipcSend("showMainWindow", {}); }, isFastPanelWindowShown() { return ipcSendSync("isFastPanelWindowShown"); }, showFastPanelWindow() { ipcSend("showFastPanelWindow", {}); }, hideFastPanelWindow() { ipcSend("hideFastPanelWindow", {}); }, showOpenDialog(options: { title?: string; defaultPath?: string; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; properties?: Array< | "openFile" | "openDirectory" | "multiSelections" | "showHiddenFiles" | "createDirectory" | "promptToCreate" | "noResolveAliases" | "treatPackageAsDirectory" | "dontAddToRecent" >; message?: string; securityScopedBookmarks?: boolean; }): string[] | undefined { return ipcSendSync("showOpenDialog", options); }, showSaveDialog(options: { title?: string; defaultPath?: string; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; message?: string; nameFieldLabel?: string; showsTagField?: string; properties?: Array< | "showHiddenFiles" | "createDirectory" | "treatPackageAsDirectory" | "showOverwriteConfirmation" | "dontAddToRecent" >; securityScopedBookmarks?: boolean; }): string | undefined { return ipcSendSync("showSaveDialog", options); }, setExpendHeight(height: number) { ipcSend("setExpendHeight", height); }, setSubInput( onChange: Function, placeholder: string = "", isFocus: boolean = true, isVisible: boolean = true, ) { if (typeof onChange === "function") { FocusAny.hooks.onSubInputChange = onChange; } ipcSendSync("setSubInput", { placeholder, isFocus, isVisible, }); }, removeSubInput() { delete FocusAny.hooks.onSubInputChange; ipcSendSync("removeSubInput"); }, setSubInputValue(text: string) { ipcSendSync("setSubInputValue", { text }); }, subInputBlur() { ipcSendSync("subInputBlur"); }, getPluginRoot() { return ipcSendSync("getPluginRoot"); }, getPluginConfig() { return ipcSendSync("getPluginConfig"); }, getPluginInfo() { return ipcSendSync("getPluginInfo"); }, getPluginEnv(): "dev" | "prod" { return ipcSendSync("getPluginEnv"); }, getQuery(requestId: string): SearchQuery { return ipcSendSync("getQuery", { requestId }); }, getPath( name: | "home" | "appData" | "userData" | "temp" | "exe" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "logs", ) { return ipcSendSync("getPath", { name }); }, showToast( body: string, options?: { duration?: number; status?: "info" | "success" | "error"; }, ): void { ipcSend("showToast", { body, options }); }, showNotification(body: string, clickActionName?: string) { ipcSend("showNotification", { body, clickActionName }); }, showMessageBox( message: string, options: { title?: string; yes?: string; no?: string; }, ) { options = options || {}; return ipcSendSync("showMessageBox", { message, ...options, }); }, copyImage(image: string) { return ipcSendSync("copyImage", { image }); }, copyText(text: string) { return ipcSendSync("copyText", { text }); }, copyFile(file: string | string[]) { return ipcSendSync("copyFile", { file }); }, getClipboardText() { return ipcSendSync("getClipboardText", {}); }, getClipboardImage() { return ipcSendSync("getClipboardImage", {}); }, getClipboardFiles(): { name: string; pathname: string; isDirectory: boolean; size: number; lastModified: number; }[] { return ipcSendSync("getClipboardFiles"); }, async listClipboardItems(option?: { limit?: number }): Promise< { type: "file" | "image" | "text"; timestamp: number; files?: FileItem[]; image?: string; text?: string; }[] > { return ipcSendAsync("listClipboardItems", { option }); }, async deleteClipboardItem(timestamp: number): Promise { return ipcSendAsync("deleteClipboardItem", { timestamp }); }, async clearClipboardItems(): Promise { return ipcSendAsync("clearClipboardItems"); }, shellOpenExternal(url: string) { shell.openExternal(url).then(); }, shellOpenPath(path: string) { shell.openPath(path).then(); }, shellShowItemInFolder(path: string) { ipcSend("shellShowItemInFolder", { path }); }, shellBeep() { ipcSend("shellBeep"); }, getFileIcon(path: string) { return ipcSendSync("getFileIcon", { path }); }, simulate: { keyboardTap( key: string, modifiers: ("ctrl" | "shift" | "command" | "option" | "alt")[], ) { ipcSend("simulateKeyboardTap", { key, modifiers }); }, typeString(text: string) { ipcSend("simulateTypeString", { text }); }, mouseToggle(type: "down" | "up", button: "left" | "right" | "middle") { ipcSend("simulateMouseToggle", { type, button }); }, mouseMove(x: number, y: number) { ipcSend("simulateMouseMove", { x, y }); }, mouseClick(button: "left" | "right" | "middle", double?: boolean) { ipcSend("simulateMouseClick", { button, double }); }, }, getCursorScreenPoint() { return electronRemote.screen.getCursorScreenPoint(); }, getDisplayNearestPoint(point: { x: number; y: number }) { return electronRemote.screen.getDisplayNearestPoint(point); }, createBrowserWindow( url: string, options: BrowserWindow.InitOptions, callback?: () => void, ) { // console.log('createBrowserWindow', JSON.stringify(url)) const pluginRoot = this.getPluginRoot(); // console.log('createBrowserWindow', JSON.stringify(url)) let preloadPath = null; options = (options || {}) as BrowserWindow.InitOptions; if (options.webPreferences && options.webPreferences.preload) { preloadPath = path.join(pluginRoot, options.webPreferences.preload); } let win = new electronRemote.BrowserWindow({ useContentSize: true, resizable: true, title: options.title || t("plugin.newWindow"), show: true, backgroundColor: "#fff", ...options, webPreferences: { webSecurity: false, backgroundThrottling: false, contextIsolation: false, webviewTag: true, nodeIntegration: true, spellcheck: false, partition: null, ...(options.webPreferences || {}), preload: preloadPath, }, }); if ( url.startsWith("file://") || url.startsWith("http://") || url.startsWith("https://") ) { win.loadURL(url); } else { win.loadFile(url); } win.on("closed", () => { win = undefined; }); win.once("ready-to-show", () => { win.show(); }); win.webContents.on("dom-ready", () => { callback && callback(); }); return win; }, screenCapture(cb: (imgBase64: string) => void): void { FocusAny.hooks.onScreenCapture = (data: { image: string }) => { // console.log('onScreenCapture', data) cb && cb(data.image); }; ipcSendSync("screenCapture"); }, getNativeId(): string { return ipcSendSync("getNativeId"); }, getAppVersion(): string { return ipcSendSync("getAppVersion"); }, outPlugin() { ipcSend("outPlugin"); }, isDarkColors() { return ipcSendSync("isDarkColors"); }, redirect(keywordsOrAction: string | string[], query?: SearchQuery): void { ipcSend("redirect", { keywordsOrAction, query }); }, getActions(names?: string[]): PluginAction[] { return ipcSendSync("getActions", { names }); }, setAction(action: PluginAction | PluginAction[]) { ipcSendSync("setAction", { action }); }, removeAction(name: string) { ipcSendSync("removeAction", { name }); }, sendBackendEvent( event: string, data?: any, option?: { timeout: number; }, ): Promise { option = Object.assign({ timeout: 10 * 1000, }); return new Promise((resolve, reject) => { const id = StrUtil.randomString(16); const timeoutTimer = setTimeout(() => { ipcRenderer.removeAllListeners(`FocusAny.Event.${id}`); reject(new Error("timeout")); }, option.timeout); ipcRenderer.once(`FocusAny.Event.${id}`, (_event, result) => { clearTimeout(timeoutTimer); resolve(result); }); ipcRenderer.send("FocusAny.Event", { id, event, data, }); setTimeout(() => { resolve(null); }, 1000); }); }, registerCallPage( type: string, callback: ( resolve: (data: any) => void, reject: (error: string) => void, data: any, ) => void, option?: { timeout?: number; }, ) { option = Object.assign( { timeout: 30 * 1000, }, option || {}, ); if (!("__page" in window)) { (window as any)["__page"] = {}; } if (!("callPage" in (window as any)["__page"])) { (window as any)["__page"].callPage = {}; } (window as any)["__page"].callPage[type] = callback; }, callPage( type: string, data?: any, option?: { timeout?: number; }, ): Promise { throw new Error("Only can be called in backend.cjs"); }, setRemoteWebRuntime(info: { userAgent: string; urlMap: Record; types: string[]; domains: string[]; blocks: string[]; }): Promise { return ipcSendAsync("setRemoteWebRuntime", { info }); }, llmListModels(): Promise< { providerId: string; providerLogo: string; providerTitle: string; modelId: string; modelName: string; }[] > { return ipcSendAsync("llmListModels"); }, llmChat(callInfo: { providerId: string; modelId: string; message: string; }): Promise<{ code: number; msg: string; data?: { message: string; }; }> { return ipcSendAsync("llmChat", { callInfo }); }, logInfo(label: string, data?: any): void { ipcSend("logInfo", { label, logData: data }); }, logError(label: string, data?: any): void { ipcSend("logError", { label, logData: data }); }, logPath(): Promise { return ipcSendSync("logPath"); }, logShow(): void { ipcSend("logShow"); }, async addLaunch( keyword: string, name: string, hotkey: HotkeyType, ): Promise { return ipcSendAsync("addLaunch", { keyword, name, hotkey }); }, async removeLaunch(keyword: string): Promise { return ipcSendAsync("removeLaunch", { keyword }); }, async activateLatestWindow(): Promise { return ipcSendAsync("activateLatestWindow"); }, showUserLogin() { ipcSend("showUserLogin"); }, getUser() { return ipcSendSync("getUser"); }, getUserAccessToken(): Promise<{ token: string; expireAt: number }> { return ipcSendAsync("getUserAccessToken"); }, listGoods(query?: { ids?: string[] }): Promise< { id: string; title: string; cover: string; priceType: "fixed" | "dynamic"; fixedPrice: string; description: string; }[] > { return ipcSendAsync("listGoods", { query }); }, openGoodsPayment(options: { goodsId: string; price?: string; outOrderId?: string; outParam?: string; }): Promise<{ paySuccess: boolean; }> { return ipcSendAsync("openGoodsPayment", { options }); }, queryGoodsOrders(options: { goodsId?: string; page?: number; pageSize?: number; }): Promise<{ page: number; total: number; records: { id: string; goodsId: string; status: "Paid" | "Unpaid"; }[]; }> { return ipcSendAsync("queryGoodsOrders", { options }); }, apiPost( url: string, body: any, option: {}, ): Promise<{ code: number; msg: string; data: any; }> { return ipcSendAsync("apiPost", { url, body, option }); }, file: { exists(path: string): Promise { return ipcSendAsync("fileExists", { path }); }, read( path: string, format?: "string" | "buffer" | "base64", ): Promise { return ipcSendAsync("fileRead", { path, format }); }, write( path: string, data: string | Uint8Array, option?: { isBase64?: boolean; }, ): Promise { return ipcSendAsync("fileWrite", { path, data, option }); }, remove(path: string): Promise { return ipcSendAsync("fileRemove", { path }); }, ext(path: string): Promise { return ipcSendAsync("fileExt", { path }); }, writeTemp( ext: string, data: string | Uint8Array, option?: { isBase64?: boolean; }, ): Promise { return ipcSendAsync("fileWriteTemp", { ext, data, option }); }, }, db: { put(doc: DbDoc) { return ipcSendSync("dbPut", { doc }); }, get>(id: string): DbDoc | null { return ipcSendSync("dbGet", { id }); }, remove(doc: string | DbDoc): DbReturn { return ipcSendSync("dbRemove", { doc }); }, bulkDocs(docs: DbDoc[]): DbReturn[] { return ipcSendSync("dbBulkDocs", { docs }); }, allDocs>(key?: string): DbDoc[] { return ipcSendSync("dbAllDocs", { key }); }, postAttachment( docId: string, attachment: Buffer | Uint8Array, type: string, ): DbReturn { return ipcSendSync("dbPostAttachment", { docId, attachment, type, }); }, getAttachment(docId: string): Uint8Array | null { return ipcSendSync("dbGetAttachment", { docId }); }, getAttachmentType(docId: string): string | null { return ipcSendSync("dbGetAttachmentType", { docId }); }, }, dbStorage: { setItem(key: string, value: any) { return ipcSendSync("dbStorageSetItem", { key, value: JSON.parse(JSON.stringify(value)), }); }, getItem(key: string) { return ipcSendSync("dbStorageGetItem", { key }); }, removeItem(key: string) { return ipcSendSync("dbStorageRemoveItem", { key }); }, }, view: { setHeight(height: number) { ipcSendToHost("view.setHeight", { height }).then(); }, getHeight(): Promise { return ipcSendToHost("view.getHeight", {}, true); }, }, fad: { async read(type: string, path: string): Promise { const fileData = await ipcSendAsync("fileRead", { path }); if (!fileData) { throw t("file.notFoundOrReadFailed"); } const fileDataJson = JSON.parse(fileData); if (fileDataJson["type"] !== type) { throw t("file.unsupportedType"); } return fileDataJson["data"]; }, async write(type: string, path: string, data: any): Promise { const fileData = { type, data, }; const fileDataJson = JSON.stringify(fileData, null, 2); await ipcSendAsync("fileWrite", { path, data: fileDataJson }); }, }, detach: { setTitle(title: string) { ipcSend("detachSetTitle", { title }); }, setOperates( operates: { name: string; title: string; click: () => void; }[], ) { const cleanOperates = operates.map((o) => { return { name: o.name, title: o.title, }; }); if (!("onDetachOperateClick" in FocusAny.hooks)) { FocusAny.hooks.onDetachOperateClick = (payload: { name: string; }) => { const { name } = payload; FocusAny.hooks.detachOperates.forEach((o) => { if (o.name === name) { o.click(); } }); }; } FocusAny.hooks.detachOperates = operates.map((o) => { return { name: o.name, title: o.title, click: o.click, }; }); ipcSend("detachSetOperates", { operates: cleanOperates }); }, setPosition( position: | "center" | "right-bottom" | "left-top" | "right-top" | "left-bottom", ) { ipcSend("detachSetPosition", { position }); }, setAlwaysOnTop(alwaysOnTop: boolean) { ipcSend("detachSetAlwaysOnTop", { alwaysOnTop }); }, setSize(width: number, height: number) { ipcSend("detachSetSize", { width, height }); }, }, util: { randomString(length: number): string { return StrUtil.randomString(length); }, bufferToBase64(buffer: Buffer): string { return FileUtil.bufferToBase64(buffer); }, base64ToBuffer(base64: string): Buffer { return FileUtil.base64ToBuffer(base64); }, datetimeString(): string { return TimeUtil.datetimeString(); }, base64Encode(data: any): string { return EncodeUtil.base64Encode(data); }, base64Decode(data: string): any { return EncodeUtil.base64Decode(data); }, md5(data: string): string { return EncodeUtil.md5(data); }, save( filename: string, data: string | Uint8Array, option?: { isBase64?: boolean; }, ): boolean { const path = FocusAny.showSaveDialog({ defaultPath: filename, }); if (!path) { return false; } if (option?.isBase64) { // remove prefix data:image/svg+xml;base64, if ((data as string).startsWith("data:")) { data = (data as string).split(",")[1]; } data = Buffer.from(data as string, "base64"); } fs.writeFileSync(path, data as Uint8Array); return true; }, }, }; ================================================ FILE: electron/preload/index.ts ================================================ import { ipcRenderer, webFrame } from "electron"; import { MAPI } from "../mapi/render"; import { FocusAny } from "./focusany"; webFrame.setZoomLevel(1); webFrame.setVisualZoomLevelLimits(1, 1); webFrame.setZoomFactor(1); // @ts-ignore window["focusany"] = FocusAny; MAPI.init(); window["__page"] = { hooks: {}, onShow: (cb: Function) => { window["__page"].hooks.onShow = cb; }, onHide: (cb: Function) => { window["__page"].hooks.onHide = cb; }, onMaximize: (cb: Function) => { window["__page"].hooks.onMaximize = cb; }, onUnmaximize: (cb: Function) => { window["__page"].hooks.onUnmaximize = cb; }, onEnterFullScreen: (cb: Function) => { window["__page"].hooks.onEnterFullScreen = cb; }, onLeaveFullScreen: (cb: Function) => { window["__page"].hooks.onLeaveFullScreen = cb; }, broadcastListeners: {}, onBroadcast: (type: string, cb: (data: any) => void) => { if (!(type in window["__page"].broadcastListeners)) { window["__page"].broadcastListeners[type] = []; } window["__page"].broadcastListeners[type].push(cb); }, offBroadcast: (type: string, cb: (data: any) => void) => { if (!(type in window["__page"].broadcastListeners)) { return; } window["__page"].broadcastListeners[type] = window[ "__page" ].broadcastListeners[type].filter((c) => c !== cb); }, callPage: {}, registerCallPage: ( name: string, cb: ( resolve: (data: any) => void, reject: (error: string) => void, data: any, ) => void, ) => { window["__page"].callPage[name] = cb; }, channel: {}, createChannel: (cb: (data: any) => void) => { const channel = Math.random().toString(36).substring(2); window["__page"].channel[channel] = cb; return channel; }, destroyChannel: (channel: string) => { delete window["__page"].channel[channel]; }, // onPluginInit: (cb: Function) => { window["__page"].hooks.onPluginInit = cb; }, onPluginInitReady: (cb: Function) => { window["__page"].hooks.onPluginInitReady = cb; }, onPluginAlreadyOpened: (cb: Function) => { window["__page"].hooks.onPluginAlreadyOpened = cb; }, onPluginExit: (cb: Function) => { window["__page"].hooks.onPluginExit = cb; }, onPluginDetached: (cb: Function) => { window["__page"].hooks.onPluginDetached = cb; }, onPluginState: (cb: Function) => { window["__page"].hooks.onPluginState = cb; }, onPluginCodeInit: (cb: Function) => { window["__page"].hooks.onPluginCodeInit = cb; }, onPluginCodeData: (cb: Function) => { window["__page"].hooks.onPluginCodeData = cb; }, onPluginCodeSetting: (cb: Function) => { window["__page"].hooks.onPluginCodeSetting = cb; }, onPluginCodeExit: (cb: Function) => { window["__page"].hooks.onPluginCodeExit = cb; }, onSetSubInput: (cb: Function) => { window["__page"].hooks.onSetSubInput = cb; }, onRemoveSubInput: (cb: Function) => { window["__page"].hooks.onRemoveSubInput = cb; }, onSetSubInputValue: (cb: Function) => { window["__page"].hooks.onSetSubInputValue = cb; }, onDetachSet: (cb: Function) => { window["__page"].hooks.onDetachSet = cb; }, onDetachWindowClosed: (cb: Function) => { window["__page"].hooks.onDetachWindowClosed = cb; }, ipcSendToHost: (channel: string, type: string, data?: any) => { ipcRenderer.sendToHost(channel, { type, data, }); }, ipcSend: (channel: string, type: string, data?: any) => { ipcRenderer.send(channel, { type, data, }); }, }; ipcRenderer.removeAllListeners("MAIN_PROCESS_MESSAGE"); ipcRenderer.on("MAIN_PROCESS_MESSAGE", (_event: any, payload: any) => { if ("APP_READY" === payload.type) { MAPI.init(payload.data.AppEnv); } else if ("CALL_PAGE" === payload.type) { let { type, data, option } = payload.data; option = Object.assign( { waitReadyTimeout: 10 * 1000, }, option, ); // console.log('CALL_PAGE', type, {type, data, option}) const resultEventName = `event:callPage:${payload.id}`; const send = (code: number, msg: string, data?: any) => { ipcRenderer.send(resultEventName, { code, msg, data }); }; if (!window["__page"].callPage) { console.warn("CALL_PAGE.Failed", JSON.stringify(payload)); send(-1, "error"); return; } const callPageExecute = () => { try { const maybePromise = window["__page"].callPage[type]( (resultData: any) => { send(0, "ok", resultData); }, (error: string) => { send(-1, error); }, data, ); if (maybePromise && typeof maybePromise.then === "function") { maybePromise.catch((e: any) => { console.error("CallPage.Error", e); send( -1, "CallPageExecuteError: " + (e?.message || e.toString()), ); }); } } catch (e) { console.error("CallPage.Error", e); send( -1, "CallPageExecuteError: " + (e?.message || e.toString()), ); } }; if (!window["__page"].callPage[type]) { if (option.waitReadyTimeout > 0) { const start = Date.now(); const monitor = () => { setTimeout(() => { if (!window["__page"].callPage[type]) { if (Date.now() - start > option.waitReadyTimeout) { console.warn("CALL_PAGE.Timeout", type, { type, data, option, }); send(-1, "timeout"); return; } else { monitor(); return; } } else { callPageExecute(); } }, 10); }; monitor(); return; } console.warn("CALL_PAGE.NotFound", type, { type, data, option }); send(-1, "event not found"); return; } callPageExecute(); } else if ("CHANNEL" === payload.type) { const { channel, data } = payload.data; if (!window["__page"].channel || !window["__page"].channel[channel]) { return; } window["__page"].channel[channel](data); } else if ("BROADCAST" === payload.type) { const { type, data } = payload.data; if (window["__page"].broadcastListeners[type]) { window["__page"].broadcastListeners[type].forEach( (cb: Function) => { cb(data); }, ); } } else { console.warn("UnknownMainProcessMessage", JSON.stringify(payload)); } }); ================================================ FILE: electron/preload/plugin.ts ================================================ import { FocusAny } from "./focusany"; import { ipcRenderer, webFrame } from "electron"; webFrame.setZoomLevel(1); webFrame.setVisualZoomLevelLimits(1, 1); webFrame.setZoomFactor(1); // @ts-ignore window["focusany"] = FocusAny; window["__page"] = { hooks: {}, onShow: (cb: Function) => { window["__page"].hooks.onShow = cb; }, onHide: (cb: Function) => { window["__page"].hooks.onHide = cb; }, onMaximize: (cb: Function) => { window["__page"].hooks.onMaximize = cb; }, onUnmaximize: (cb: Function) => { window["__page"].hooks.onUnmaximize = cb; }, onEnterFullScreen: (cb: Function) => { window["__page"].hooks.onEnterFullScreen = cb; }, onLeaveFullScreen: (cb: Function) => { window["__page"].hooks.onLeaveFullScreen = cb; }, broadcastListeners: {}, onBroadcast: (type: string, cb: (data: any) => void) => { if (!(type in window["__page"].broadcastListeners)) { window["__page"].broadcastListeners[type] = []; } window["__page"].broadcastListeners[type].push(cb); }, offBroadcast: (type: string, cb: (data: any) => void) => { if (!(type in window["__page"].broadcastListeners)) { return; } window["__page"].broadcastListeners[type] = window[ "__page" ].broadcastListeners[type].filter((c) => c !== cb); }, callPage: {}, registerCallPage: ( name: string, cb: ( resolve: (data: any) => void, reject: (error: string) => void, data: any, ) => void, ) => { window["__page"].callPage[name] = cb; }, channel: {}, createChannel: (cb: (data: any) => void) => { const channel = Math.random().toString(36).substring(2); window["__page"].channel[channel] = cb; return channel; }, destroyChannel: (channel: string) => { delete window["__page"].channel[channel]; }, }; ipcRenderer.removeAllListeners("MAIN_PROCESS_MESSAGE"); ipcRenderer.on("MAIN_PROCESS_MESSAGE", (_event: any, payload: any) => { if ("APP_READY" === payload.type) { } else if ("CALL_PAGE" === payload.type) { let { type, data, option } = payload.data; option = Object.assign( { waitReadyTimeout: 10 * 1000, }, option, ); // console.log('CALL_PAGE', type, {type, data, option}) const resultEventName = `event:callPage:${payload.id}`; const send = (code: number, msg: string, data?: any) => { ipcRenderer.send(resultEventName, { code, msg, data }); }; if (!window["__page"].callPage) { send(-1, "error"); return; } const callPageExecute = () => { try { const maybePromise = window["__page"].callPage[type]( (resultData: any) => { send(0, "ok", resultData); }, (error: string) => { send(-1, error); }, data, ); if (maybePromise && typeof maybePromise.then === "function") { maybePromise.catch((e: any) => { console.error("CallPage.Error", e); send( -1, "CallPageExecuteError: " + (e?.message || e.toString()), ); }); } } catch (e) { console.error("CallPage.Error", e); send( -1, "CallPageExecuteError: " + (e?.message || e.toString()), ); } }; if (!window["__page"].callPage[type]) { if (option.waitReadyTimeout > 0) { const start = Date.now(); const monitor = () => { setTimeout(() => { if (!window["__page"].callPage[type]) { if (Date.now() - start > option.waitReadyTimeout) { send(-1, "timeout"); return; } else { monitor(); return; } } else { callPageExecute(); } }, 10); }; monitor(); return; } send(-1, "event not found"); return; } callPageExecute(); } else if ("CHANNEL" === payload.type) { const { channel, data } = payload.data; if (!window["__page"].channel || !window["__page"].channel[channel]) { return; } window["__page"].channel[channel](data); } else if ("BROADCAST" === payload.type) { const { type, data } = payload.data; if (window["__page"].broadcastListeners[type]) { window["__page"].broadcastListeners[type].forEach( (cb: Function) => { cb(data); }, ); } } else { console.warn("UnknownMainProcessMessage", JSON.stringify(payload)); } }); ================================================ FILE: electron/resources/build/entitlements.mac.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables ================================================ FILE: electron-builder.json5 ================================================ // @see https://www.electron.build/configuration/configuration { "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", "appId": "FocusAny", "asar": true, "npmRebuild": true, "publish": [], "productName": "FocusAny", "directories": { "output": "dist-release", "buildResources": "electron/resources/build" }, "afterPack": "./scripts/build_optimize.cjs", "files": [ "dist", "dist-electron" ], // "extraResources": [ // { // "from": "node_modules/ffmpeg-static", // "to": "bin/ffmpeg", // } // ], "win": { icon: "electron/resources/build/logo.ico", "target": [ { "target": "nsis", "arch": [ "x64", "arm64" ] }, ], "artifactName": "${productName}-${version}-win-${arch}.${ext}", "extraResources": [ { "from": "electron/resources/extra", "to": "extra", "filter": [ "common", "win" ] } ], "protocols": [ { "name": "FocusAny Protocol", "schemes": [ "focusany" ] } ], "fileAssociations": [ { "ext": "fad", "name": "FocusAny Data File", "role": "Editor" } ] }, "nsis": { "artifactName": "${productName}-${version}-win-setup-${arch}.${ext}", "shortcutName": "${productName}", "uninstallDisplayName": "${productName}", "oneClick": false, "perMachine": false, "allowToChangeInstallationDirectory": true, "deleteAppDataOnUninstall": false }, "portable": { "artifactName": "${productName}-${version}-win-portable-${arch}.${ext}", "requestExecutionLevel": "user" }, "appx": { "identityName": "FocusAny", "publisher": "CN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX", "publisherDisplayName": "FocusAny", "languages": [ "zh-CN", "en-US", "zh-TW" ], "artifactName": "${productName}-${version}-win-appx-${arch}.${ext}", }, "mac": { "icon": "logo.icns", "target": [ { "target": "dmg", "arch": [ "x64", "arm64" ] } ], "artifactName": "${productName}-${version}-mac-${arch}.${ext}", "extraResources": [ { "from": "electron/resources/extra", "to": "extra", "filter": [ "common", "osx" ] } ], "x64ArchFiles": "Contents/Resources/extra/**/*", "entitlementsInherit": "./entitlements.mac.plist", "entitlements": "./entitlements.mac.plist", "extendInfo": { "NSDocumentsFolderUsageDescription": "Application requests access to the user's Documents folder.", "NSDownloadsFolderUsageDescription": "Application requests access to the user's Downloads folder.", "NSAccessibilityUsageDescription": "Application requests access to the user's Accessibility features.", }, "protocols": [ { "name": "FocusAny Protocol", "schemes": [ "focusany" ] } ], "fileAssociations": [ { "ext": "fad", "name": "FocusAny Data File", "role": "Editor" } ], "electronLanguages": [ "zh-CN", "en-US" ], "type": "development", "notarize": false, "darkModeSupport": false, "hardenedRuntime": true, "gatekeeperAssess": false, "identity": "Xi'an Yanyi Information Technology Co., Ltd (Q96H3H33RK)", }, "linux": { "icon": "logo.icns", "maintainer": "FocusAny", "category": "Utility", "target": [ { "target": "AppImage", "arch": [ "x64", "arm64" ] }, { "target": "deb", "arch": [ "x64", "arm64" ] } ], "artifactName": "${productName}-${version}-linux-${arch}.${ext}", "extraResources": [ { "from": "electron/resources/extra", "to": "extra", "filter": [ "common", "linux" ] } ], "protocols": [ { "name": "FocusAny Protocol", "schemes": [ "focusany" ] } ], "fileAssociations": [ { "ext": "fad", "name": "FocusAny Data File", "role": "Editor" } ] }, "afterSign": "./scripts/notarize.cjs", } ================================================ FILE: entitlements.mac.plist ================================================ com.apple.security.app-sandbox com.apple.security.accessibility com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables com.apple.security.cs.disable-library-validation com.apple.security.device.input-monitor com.apple.security.automation.apple-events com.apple.security.device.audio-input ================================================ FILE: index.html ================================================ %name%
================================================ FILE: package.json ================================================ { "name": "focusany", "version": "1.1.0", "main": "dist-electron/main/index.js", "description": "FocusAny", "author": "ModStartLib", "homepage": "https://focusany.com", "license": "Apache-2.0", "private": true, "keywords": [ "electron", "rollup", "vite", "vue3", "vue" ], "debug": { "env": { "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/" } }, "type": "module", "scripts": { "dev": "vite", "dev:seed": "tsx test/dev-seed.ts", "dev:mac": "pkill -f focusany; vite", "dev:mac:pre": "export ELECTRON_ENV_PROD=1 && pkill -f focusany; vite", "dev:win": "chcp 65001 && vite", "dev:win:pre": "chcp 65001 && set ELECTRON_ENV_PROD=1 && vite", "dev:debug": "vite --debug", "build:preview": "npm run format && vite build", "build": "npm run format && vite build && electron-builder", "build:win": "npm run format && vite build && electron-builder --win", "build:mac": "npm run format && vite build && electron-builder --mac", "build:mac-arm": "npm run format && vite build && electron-builder --mac --arm64", "build:linux": "npm run format && vite build && electron-builder --linux", "preview": "vite preview", "format": "prettier --write --log-level warn \"src/**/*.{ts,tsx,vue,js}\" \"electron/**/*.{ts,tsx,js}\"", "lint": "eslint src electron", "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", "re-sqlite": "npx electron-rebuild -f -w better-sqlite3" }, "devDependencies": { "@arco-design/web-vue": "^2.55.3", "@electron/notarize": "^2.5.0", "@iconify-json/mdi": "^1.2.3", "@rollup/plugin-commonjs": "^28.0.2", "@types/lodash-es": "^4.17.12", "@types/pouchdb": "^6.4.2", "@types/splitpanes": "^2.2.6", "@typescript-eslint/eslint-plugin": "^8.59.2", "@typescript-eslint/parser": "^8.59.2", "@vitejs/plugin-vue": "^5.0.4", "autoprefixer": "^10.4.19", "electron": "^29.1.1", "electron-builder": "^24.13.3", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-vue": "^10.9.1", "less": "^4.2.0", "postcss": "^8.4.39", "prettier": "^3.8.3", "tailwindcss": "^3.4.4", "tsx": "^4.21.0", "typescript": "^5.4.2", "unplugin-icons": "^23.0.1", "vite": "^5.1.5", "vite-plugin-electron": "^0.28.4", "vite-plugin-electron-renderer": "^0.14.5", "vite-plugin-html": "^3.2.2", "vue": "^3.4.21", "vue-eslint-parser": "^10.4.0", "vue-tsc": "^2.0.6" }, "dependencies": { "@babel/runtime": "^7.24.8", "@codemirror/commands": "^6.1.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-python": "^6.1.6", "@codemirror/state": "^6.4.1", "@devicefarmer/adbkit": "^3.2.6", "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", "@electron/remote": "^2.1.2", "@gradio/client": "^1.7.0", "@nut-tree-fork/nut-js": "^4.2.6", "@types/showdown": "^2.0.6", "@uiw/codemirror-theme-dracula": "^4.23.0", "@uiw/codemirror-theme-quietlight": "^4.23.0", "@vue-js-cron/light": "^4.0.9", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "axios": "^1.7.2", "better-sqlite3": "^12.2.0", "chardet": "^2.0.0", "codemirror": "^6.0.1", "crypto": "^1.0.1", "date-and-time": "^3.4.1", "dayjs": "^1.11.12", "electron-context-menu": "^4.0.4", "express": "^5.1.0", "extract-file-icon": "^0.3.2", "ffmpeg-static": "^5.2.0", "fix-path": "^4.0.0", "get-windows": "^9.2.0", "iconv": "^3.0.1", "iconv-lite": "^0.6.3", "js-base64": "^3.7.7", "lodash-es": "^4.17.21", "memorystream": "^0.3.1", "node-window-manager": "^2.2.4", "nodejs-base64": "^2.0.0", "original-fs": "^1.2.0", "pinia": "^2.1.7", "pinyin-match": "^1.2.6", "pouchdb": "^9.0.0", "pouchdb-load": "^1.4.6", "pouchdb-replication-stream": "^1.2.9", "qrcode": "^1.5.4", "showdown": "^2.1.0", "splitpanes": "^3.1.5", "systeminformation": "^5.25.11", "tiny-emitter": "^2.1.0", "uiohook-napi": "^1.5.4", "vue-command": "^35.2.1", "vue-i18n": "^9.13.1", "vue-router": "^4.4.0", "wavesurfer.js": "^7.8.6", "webdav": "^5.7.1", "xgplayer": "^3.0.20", "yauzl": "^3.1.3" }, "optionalDependencies": { "@electron/osx-sign": "^1.3.2", "electron-clipboard-ex": "^1.3.3", "node-mac-permissions": "^2.4.0" }, "repository": { "type": "git", "url": "https://github.com/modstart-lib/focusany.git" } } ================================================ FILE: page/about.html ================================================ %name%
================================================ FILE: page/detachWindow.html ================================================ %name%
================================================ FILE: page/fastPanel.html ================================================ %name%
================================================ FILE: page/feedback.html ================================================ %name%
================================================ FILE: page/guide.html ================================================ %name%
================================================ FILE: page/log.html ================================================ %name%
================================================ FILE: page/monitor.html ================================================ %name%
================================================ FILE: page/payment.html ================================================ %name%
================================================ FILE: page/setup.html ================================================ %name%
================================================ FILE: page/store.html ================================================ %name%
================================================ FILE: page/system.html ================================================ %name%
================================================ FILE: page/user.html ================================================ %name%
================================================ FILE: page/workflow.html ================================================ %name%
================================================ FILE: postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: public/iconfont/iconfont.css ================================================ @font-face { font-family: "iconfont"; /* Project id 4733566 */ src: url('iconfont.woff2?t=1736411931319') format('woff2'), url('iconfont.woff?t=1736411931319') format('woff'), url('iconfont.ttf?t=1736411931319') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-backend:before { content: "\e61c"; } .icon-command:before { content: "\e6aa"; } .icon-view:before { content: "\e6e1"; } .icon-code:before { content: "\e6b0"; } .icon-info:before { content: "\e72a"; } .icon-success:before { content: "\e72d"; } .icon-text:before { content: "\e959"; } .icon-close-o:before { content: "\e6a7"; } .icon-pin:before { content: "\e863"; } .icon-store:before { content: "\e670"; } .icon-sound:before { content: "\e62a"; } .icon-desktop:before { content: "\e8e9"; } .icon-network:before { content: "\e675"; } .icon-avatar:before { content: "\e604"; } .icon-empty-box:before { content: "\e620"; } .icon-github:before { content: "\e732"; } .icon-gitee:before { content: "\e601"; } .icon-close:before { content: "\e61b"; } .icon-min:before { content: "\e67a"; } .icon-max:before { content: "\e665"; } ================================================ FILE: public/iconfont/iconfont.js ================================================ window._iconfont_svg_string_4733566='',(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("")}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); ================================================ FILE: public/iconfont/iconfont.json ================================================ { "id": "4733566", "name": "FocusAny", "font_family": "iconfont", "css_prefix_text": "icon-", "description": "", "glyphs": [ { "icon_id": "38207392", "name": "backend", "font_class": "backend", "unicode": "e61c", "unicode_decimal": 58908 }, { "icon_id": "35089476", "name": "command", "font_class": "command", "unicode": "e6aa", "unicode_decimal": 59050 }, { "icon_id": "321908", "name": "view", "font_class": "view", "unicode": "e6e1", "unicode_decimal": 59105 }, { "icon_id": "5428337", "name": "code", "font_class": "code", "unicode": "e6b0", "unicode_decimal": 59056 }, { "icon_id": "19418435", "name": "info", "font_class": "info", "unicode": "e72a", "unicode_decimal": 59178 }, { "icon_id": "7009016", "name": "success", "font_class": "success", "unicode": "e72d", "unicode_decimal": 59181 }, { "icon_id": "924549", "name": "text", "font_class": "text", "unicode": "e959", "unicode_decimal": 59737 }, { "icon_id": "257406", "name": "close-o", "font_class": "close-o", "unicode": "e6a7", "unicode_decimal": 59047 }, { "icon_id": "34453257", "name": "pin", "font_class": "pin", "unicode": "e863", "unicode_decimal": 59491 }, { "icon_id": "32804187", "name": "store", "font_class": "store", "unicode": "e670", "unicode_decimal": 58992 }, { "icon_id": "663540", "name": "sound", "font_class": "sound", "unicode": "e62a", "unicode_decimal": 58922 }, { "icon_id": "924409", "name": "desktop", "font_class": "desktop", "unicode": "e8e9", "unicode_decimal": 59625 }, { "icon_id": "6537202", "name": "network", "font_class": "network", "unicode": "e675", "unicode_decimal": 58997 }, { "icon_id": "30808030", "name": "avatar", "font_class": "avatar", "unicode": "e604", "unicode_decimal": 58884 }, { "icon_id": "14027553", "name": "empty-box", "font_class": "empty-box", "unicode": "e620", "unicode_decimal": 58912 }, { "icon_id": "7239764", "name": "github", "font_class": "github", "unicode": "e732", "unicode_decimal": 59186 }, { "icon_id": "39287937", "name": "gitee", "font_class": "gitee", "unicode": "e601", "unicode_decimal": 58881 }, { "icon_id": "1115039", "name": "close", "font_class": "close", "unicode": "e61b", "unicode_decimal": 58907 }, { "icon_id": "1649166", "name": "min", "font_class": "min", "unicode": "e67a", "unicode_decimal": 59002 }, { "icon_id": "1818719", "name": "max", "font_class": "max", "unicode": "e665", "unicode_decimal": 58981 } ] } ================================================ FILE: public/static/pluginEmpty.html ================================================ Plugin Empty View
Plugin Empty View
================================================ FILE: scripts/build_optimize.cjs ================================================ const common = require("./common.cjs"); console.log("BuildOptimize", { name: common.platformName(), arch: common.platformArch(), }); exports.default = async function (context) { console.log("BuildOptimize.output", { context: context, root: context.appOutDir, }); // copy extra electron/resources/extra/[name]-[arch] to extra const platformName = common.platformName(); const platformArch = common.platformArch(); const name = platformName + "-" + platformArch; const srcDir = `electron/resources/extra/${name}`; let destDir = null; if (platformName === 'osx') { destDir = common.pathResolve( context.appOutDir, `${context.packager.appInfo.productFilename}.app`, "Contents", "Resources", "extra", name ); } else if (platformName === 'win') { destDir = common.pathResolve(context.appOutDir, "resources", "extra", name); } else if (platformName === 'linux') { destDir = common.pathResolve(context.appOutDir, "resources", "extra", name); } console.log("BuildOptimize.copy", { platformName, platformArch, srcDir, destDir, }); if (srcDir && common.exists(srcDir)) { console.log(`Copying from ${srcDir} to ${destDir}`); common.copy(srcDir, destDir, true); console.log(`Copy completed`); } else { console.log(`No matching source directory found for platform: ${platformName}-${platformArch}`); } // common.listFiles(context.appOutDir, true).forEach((p) => { // console.log('BuildOptimize.path', (p.isDir ? 'D:' : 'F:') + p.path); // }) // const localeDir = context.appOutDir + "/AigcPanel.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/"; // console.log(`localeDir: ${localeDir}`); // fs.readdir(localeDir, function (err, files) { // if (!(files && files.length)) { // return; // } // for (let f of files) { // if (f.endsWith('.lproj')) { // if (!(f.startsWith("en") || f.startsWith("zh"))) { // const p = localeDir + f; // console.log(`removeFile: ${p}`); // fs.rmdirSync(p, {recursive: true}); // } // } // } // }); }; ================================================ FILE: scripts/common.cjs ================================================ const fs = require("node:fs"); const {resolve, join} = require("node:path"); const crypto = require("node:crypto"); const dir = (p) => { p = p || '' return join(__dirname, "../" + p) } const distReleaseDir = (p) => { if (p) { return dir("dist-release/" + p) } else { return dir("dist-release") } } function calcSha256File(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash("sha256"); const stream = fs.createReadStream(filePath); stream.on("data", (data) => hash.update(data)); stream.on("end", () => resolve(hash.digest("hex"))); stream.on("error", reject); }); } const platformName = () => { switch (process.platform) { case "darwin": return "osx"; case "win32": return "win"; case "linux": return "linux"; } return null; } const platformArch = () => { switch (process.arch) { case "x64": return "x86"; case "arm64": return "arm64"; } return null; } const listFiles = (dir, recursive, regex) => { regex = regex || null recursive = recursive || false const files = fs.readdirSync(dir); const list = []; for (let f of files) { const p = resolve(dir, f); if (regex) { if (!regex.test(p)) { continue; } } const stat = fs.statSync(p); list.push({ isDir: stat.isDirectory(), name: f, path: p }); if (recursive && stat.isDirectory()) { list.push(...listFiles(p, recursive)); } } return list; } const copy = (src, dest, print) => { print = print || false if (!fs.existsSync(src)) { console.warn(`Source path does not exist: ${src}`); return; } if (fs.statSync(src).isDirectory()) { fs.mkdirSync(dest, {recursive: true}); const files = fs.readdirSync(src); for (const file of files) { copy(join(src, file), join(dest, file)); } } else { if (print) { console.log(`Copying file from ${src} to ${dest}`); } fs.copyFileSync(src, dest); } } const pathResolve = (...args)=>{ return resolve(...args) } const exists = (p) => { try { return fs.existsSync(p); } catch (e) { return false; } } async function calcSha256() { console.log('calcSha256.start') const results = [] const files = listFiles(distReleaseDir(), false, /\.(exe|dmg|AppImage|deb)$/) for (const p of files) { const sha256 = await calcSha256File(p.path); results.push({ name: p.name, sha256: sha256 }) } const target = distReleaseDir(`sha256-${platformName()}-${platformArch()}.yml`) const content = results.map((r) => { return `${r.name}: ${r.sha256}` }).join("\n") fs.writeFileSync(target, content); console.log('calcSha256.end', target, results) } module.exports = { dir, distReleaseDir, platformName, platformArch, listFiles, copy, pathResolve, exists, calcSha256, } ================================================ FILE: scripts/icon_convert.sh ================================================ #!/bin/bash # prepare # brew install --cask inkscape echo "Convert icon" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT_ROOT=$(realpath "${DIR}/..") echo "PROJECT_ROOT: ${PROJECT_ROOT}" path_svg="${PROJECT_ROOT}/public/logo.svg" path_white_svg="${PROJECT_ROOT}/public/logo-white.svg" path_build="${PROJECT_ROOT}/electron/resources/build" path_extra="${PROJECT_ROOT}/electron/resources/extra" path_source_png="${path_build}/logo_1024x1024.png" cp -a "${path_svg}" "${PROJECT_ROOT}/src/assets/image/logo.svg" cp -a "${path_white_svg}" "${PROJECT_ROOT}/src/assets/image/logo-white.svg" cp -a "${path_white_svg}" "${PROJECT_ROOT}/src/assets/image/search-icon.svg" inkscape "${path_svg}" --export-type=png --export-filename="${path_source_png}" -w 1024 -h 1024 size=(16 32 44 48 64 128 150 256 512) for i in "${size[@]}"; do path_png="${path_build}/logo@${i}x$i.png" echo "Generate: logo@${i}x$i.png" inkscape --export-type="png" --export-filename="${path_png}" -w $i -h $i "${path_source_png}" done path_ico="${path_build}/logo.ico" echo "Generate: logo.ico" magick "${path_source_png}" -define icon:auto-resize=256,48,32,16 "${path_ico}" echo "Generate: logo.png" rm -rf "${path_build}/logo.png" cp -a "${path_build}/logo@256x256.png" "${path_build}/logo.png" echo "Generate: logo.icns" path_iconset="${path_build}/icon.iconset" rm -rf "${path_iconset}" mkdir -p "${path_iconset}" cp -a "${path_build}/logo@256x256.png" "${path_iconset}/icon_256x256.png" cp -a "${path_build}/logo@32x32.png" "${path_iconset}/icon_32x32.png" cp -a "${path_build}/logo@16x16.png" "${path_iconset}/icon_16x16.png" iconutil -c icns "${path_iconset}" -o "${path_build}/logo.icns" echo "Generate: appx/StoreLogo.png" cp -a "${path_build}/logo@256x256.png" "${path_build}/appx/StoreLogo.png" echo "Generate: appx/Square44x44Logo.png" cp -a "${path_build}/logo@44x44.png" "${path_build}/appx/Square44x44Logo.png" echo "Generate: appx/Square150x150Logo.png" cp -a "${path_build}/logo@150x150.png" "${path_build}/appx/Square150x150Logo.png" echo "Generate: appx/Wide310x150Logo.png" magick "${path_build}/logo@150x150.png" -resize 310x150 -background none -gravity center -extent 310x150 "${path_build}/appx/Wide310x150Logo.png" echo "Generate: common/tray/icon.png" mkdir -p "${path_extra}/common/tray" cp -a "${path_build}/logo@256x256.png" "${path_extra}/common/tray/icon.png" echo "Generate: common/tray/icon.ico" cp -a "${path_build}/logo.ico" "${path_extra}/common/tray/icon.ico" echo "Generate: osx/tray/iconTemplate.png" mkdir -p "${path_extra}/osx/tray" magick "${path_build}/logo-gray.png" -resize 16x16 -background none -gravity center "${path_extra}/osx/tray/iconTemplate.png" echo "Generate: osx/tray/iconTemplate@2x.png" magick "${path_build}/logo-gray.png" -resize 32x32 -background none -gravity center "${path_extra}/osx/tray/iconTemplate@2x.png" echo "Generate: osx/tray/iconTemplate@4x.png" magick "${path_build}/logo-gray.png" -resize 64x64 -background none -gravity center "${path_extra}/osx/tray/iconTemplate@4x.png" rm -rf "${path_iconset}" rm -rf ${path_build}/logo@* ================================================ FILE: scripts/init.sh ================================================ #!/usr/bin/env bash set -euo pipefail REPO_URL="https://github.com/modstart-lib/share-binary" REPO_DIR="share-binary" if [ ! -d "$REPO_DIR/.git" ]; then echo "🔹 目录不存在,正在克隆仓库..." git clone "$REPO_URL" else echo "🔹 仓库已存在,进入目录并更新..." cd "$REPO_DIR" git pull origin main cd .. fi #rm -rfv electron/resources/extra/osx-arm64 #mkdir -p electron/resources/extra/osx-arm64 #cp -a share-binary/osx-arm64/scrcpy electron/resources/extra/osx-arm64/scrcpy #cp -a share-binary/osx-arm64/ffmpeg electron/resources/extra/osx-arm64/ffmpeg #cp -a share-binary/osx-arm64/ffprobe electron/resources/extra/osx-arm64/ffprobe # #rm -rfv electron/resources/extra/osx-x86 #mkdir -p electron/resources/extra/osx-x86 #cp -a share-binary/osx-x86/ffmpeg electron/resources/extra/osx-x86/ffmpeg #cp -a share-binary/osx-x86/ffprobe electron/resources/extra/osx-x86/ffprobe #rm -rfv electron/resources/extra/linux-arm64 #mkdir -p electron/resources/extra/linux-arm64 #cp -a share-binary/linux-arm64/scrcpy electron/resources/extra/linux-arm64/scrcpy #cp -a share-binary/linux-arm64/ffmpeg electron/resources/extra/linux-arm64/ffmpeg #cp -a share-binary/linux-arm64/ffprobe electron/resources/extra/linux-arm64/ffprobe #rm -rfv electron/resources/extra/linux-x86 #mkdir -p electron/resources/extra/linux-x86 #cp -a share-binary/linux-x86/scrcpy electron/resources/extra/linux-x86/scrcpy #cp -a share-binary/linux-x86/ffmpeg electron/resources/extra/linux-x86/ffmpeg #cp -a share-binary/linux-x86/ffprobe electron/resources/extra/linux-x86/ffprobe rm -rfv electron/resources/extra/win-x86 mkdir -p electron/resources/extra/win-x86 cp -a share-binary/win-x86/ScreenCapture.exe electron/resources/extra/win-x86/ScreenCapture.exe #cp -a share-binary/win-x86/ffmpeg.exe electron/resources/extra/win-x86/ffmpeg.exe #cp -a share-binary/win-x86/ffprobe.exe electron/resources/extra/win-x86/ffprobe.exe # ls -R electron/resources/extra ================================================ FILE: scripts/notarize.cjs ================================================ const {notarize} = require("@electron/notarize"); const common = require('./common.cjs') exports.default = async function notarizing(context) { const appName = context.packager.appInfo.productFilename; const {electronPlatformName, appOutDir} = context; console.log(` • Notarization Start`); // We skip notarization if the process is not running on MacOS and // if the enviroment variable SKIP_NOTARIZE is set to `true` // This is useful for local testing where notarization is useless if ( electronPlatformName !== "darwin" || process.env.SKIP_NOTARIZE === "true" ) { console.log(` • Skipping notarization`); return; } // THIS MUST BE THE SAME AS THE `appId` property // in your electron builder configuration const appId = "FocusAny"; let appPath = `${appOutDir}/${appName}.app`; let {APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID} = process.env; if (!APPLE_ID) { console.info(" • Notarization ignore: APPLE_ID is empty"); await common.calcSha256() return; } const notarizeOption = { tool: "notarytool", appBundleId: appId, appPath, appleId: APPLE_ID, appleIdPassword: APPLE_ID_PASSWORD, teamId: APPLE_TEAM_ID, verbose: true, } console.log(` • Notarizing`, `appPath:${appPath} notarizeOption:${JSON.stringify(notarizeOption)}`); try { const result = await notarize(notarizeOption); console.log(" • Notarization successful!"); await common.calcSha256() return result; } catch (error) { console.error(" • Notarization failed:", error.message); console.error(" • Stack trace:", error.stack); await common.calcSha256() throw new Error(`Notarization failed: ${error.message}`); } }; ================================================ FILE: sdk/.babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "targets": { "browsers": [ "> 1%", "last 2 versions", "not dead" ] }, "modules": false, "useBuiltIns": false } ], [ "@babel/preset-typescript", { "allowDeclareFields": true } ] ], "plugins": [] } ================================================ FILE: sdk/.github/workflows/tag-release.yml ================================================ name: Publish to npm on: push: tags: - "v*.*.*" # 仅当推送匹配 vX.X.X 的标签时触发 jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: "20" - name: Install dependencies run: npm install && npm run build - name: Authenticate to npm run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish package run: npm publish --access public ================================================ FILE: sdk/.gitignore ================================================ *.tgz *.js ================================================ FILE: sdk/.npmignore ================================================ example/ *.tgz tests/ ================================================ FILE: sdk/.nvmrc ================================================ 20 ================================================ FILE: sdk/README.md ================================================ # FocusAny SDK TypeScript definitions and utilities for FocusAny. ## Installation ```bash npm install focusany-sdk ``` ## CLI Tools ### FocusAny CLI The SDK now provides a unified command-line interface through the `focusany` command. ```bash npx focusany [options] ``` Available commands: - `release-prepare`: Check and update config.json for production release - `version`: Display the current version of FocusAny SDK - `help`: Show help information ### Release Prepare #### Basic Usage ```bash npx focusany release-prepare ``` This will check `dist/config.json` in your current directory. #### Custom Config Path ```bash npx focusany release-prepare path/to/your/config.json ``` This command will: - Look for the specified config file (or `dist/config.json` by default) - Check if `development.env` is set to `"dev"` - Automatically change it to `"prod"` if needed - Display appropriate messages about the changes made #### Usage Examples **Release Prepare - Default config file:** ```bash npx focusany release-prepare ``` **Release Prepare - Custom config file:** ```bash npx focusany release-prepare build/config.json npx focusany release-prepare src/configs/app-config.json ``` **Version Command:** ```bash npx focusany version ``` ## Development ### Building ```bash npm run build ``` This will build both the shim files and the CLI tools. ### Building CLI only ```bash npm run build:cli ``` ### Building shim only ```bash npm run build:shim ``` ## License Apache-2.0 ================================================ FILE: sdk/bin/command.ts ================================================ #!/usr/bin/env node import * as fs from "fs"; import * as path from "path"; import * as process from "process"; interface Config { development?: { env?: string; [key: string]: any; }; [key: string]: any; } // Command handler for release-prepare function releasePrepare(args: string[]) { const customConfigPath = args[0]; // Get the project root directory that calls this command (not the SDK package directory) const cwd = process.cwd(); const configPath = customConfigPath ? path.resolve(cwd, customConfigPath) : path.resolve(cwd, "dist/config.json"); const configDir = path.dirname(configPath); console.log("🔍 Release Prepare"); if (customConfigPath) { console.log(`Using custom config file path: ${customConfigPath}`); } console.log(`Checking config file: ${configPath}`); if (!fs.existsSync(configPath)) { console.warn(`❌ Configuration file not found ${configPath}`); process.exit(1); } let json: Config | null = null; let jsonChanged = false; try { const configContent = fs.readFileSync(configPath, "utf-8"); json = JSON.parse(configContent); } catch (error) { console.error( "❌ Error reading or parsing configuration file:", (error as Error).message ); process.exit(1); } if (!json) { console.error("❌ Error parsing configuration file, json is null"); process.exit(1); } if (json.development && json.development.env === "dev") { console.warn( `⚠️ Detected env field in config.json is "dev", it has been changed to "prod"` ); json.development.env = "prod"; jsonChanged = true; } if (jsonChanged) { fs.writeFileSync(configPath, JSON.stringify(json, null, 4), "utf-8"); console.log("✅ config.json file has been updated"); } console.log("🎉 Release prepare completed"); } // Command handler for version function version() { try { const packageJsonPath = path.resolve(__dirname, "../package.json"); const packageJson = JSON.parse( fs.readFileSync(packageJsonPath, "utf-8") ); console.log(`🔖 FocusAny SDK Version: ${packageJson.version}`); } catch (error) { console.error( "❌ Error reading version information:", (error as Error).message ); process.exit(1); } } // Command handler for help function showHelp() { console.log(` 🚀 FocusAny SDK CLI Usage: npx focusany [options] Commands: release-prepare [path] Check and update config.json for production release version Display the current version of FocusAny SDK help Show this help message Examples: npx focusany release-prepare npx focusany release-prepare path/to/config.json npx focusany version `); } // Main command router function main() { const args = process.argv.slice(2); const command = args.shift() || "help"; console.log("🚀 [FocusAny SDK] Start"); switch (command) { case "release-prepare": releasePrepare(args); break; case "version": version(); break; case "help": showHelp(); break; default: console.error(`❌ Unknown command: ${command}`); showHelp(); process.exit(1); } console.log("🚀 [FocusAny SDK] End"); } // Execute the main function main(); ================================================ FILE: sdk/config.schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://focusany.com/sdk/config.schema.json", "title": "FocusAny 插件配置文件", "description": "FocusAny 插件配置文件,用于描述插件的基本信息,以及插件的入口文件。", "type": "object", "properties": { "name": { "type": "string", "description": "插件名称,此为必选项,且插件应用内不可重复。" }, "version": { "type": "string", "description": "插件版本,此为必选项。" }, "platform": { "type": "array", "description": "支持的平台,此为选填,留空表示支持所有平台。", "items": { "type": "string", "enum": [ "win", "osx", "linux" ] } }, "versionRequire": { "type": "string", "description": "FocusAny 版本要求,如 * 或 >=1.0.0 或 <=1.0.0 或 >1.0.0 或 <1.0.0,此为选填,留空表示不限制 FocusAny 版本。" }, "editionRequire": { "type": "array", "description": "FocusAny 类型要求,此为选填,留空表示不限制 FocusAny 类型(open社区版、pro专业版)。", "items": { "type": "string", "enum": [ "open", "pro" ] } }, "title": { "type": "string", "description": "插件标题,此为必选项。" }, "logo": { "type": "string", "description": "插件图标,支持png,jpg,svg格式" }, "description": { "type": "string", "description": "插件描述" }, "main": { "type": "string", "description": "主入口文件" }, "mainView": { "type": "string", "description": "快捷面板/智能视图 入口文件,当该配置为空时,使用主入口文件" }, "preload": { "type": "string", "description": "插件预加载文件,相对于插件目录,需要是 cjs 文件" }, "author": { "type": "string", "description": "插件作者" }, "homepage": { "type": "string", "description": "插件主页" }, "actions": { "type": "array", "description": "插件动作,描述了当 FocusAny 主输入框内容产生变化时,此插件应用是否显示在搜索结果列表中,一个插件应用可以有多个功能,一个功能可以提供多个命令供用户搜索。", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "插件应用提供的某个功能的唯一标示,此为必选项,且插件应用内不可重复。" }, "title": { "type": "string", "description": "对此功能的说明,将在搜索列表对应位置中显示。" }, "icon": { "type": "string", "description": "此功能的图标,支持png,jpg,svg格式。" }, "trackHistory": { "type": "boolean", "description": "是否启用历史记录,默认为 true。启用后,FocusAny 会记录用户对该功能的使用历史,并在搜索结果中显示。" }, "type": { "type": "string", "description": "此功能的类型", "enum": [ "command", "web", "code", "backend", "view" ] }, "platform": { "type": "array", "description": "支持的平台,此为选填,留空表示支持所有平台。", "items": { "type": "string", "enum": [ "win", "osx", "linux" ] } }, "data": { "type": "object", "description": "其他补充数据。", "properties": { "command": { "type": "string", "description": "type=command时,此为必选项,表示此功能的命令。" }, "showFastPanel": { "type": "boolean", "description": "type=view时,是否在快捷面板中显示,默认为 false" }, "showMainPanel": { "type": "boolean", "description": "type=view时,是否在主面板中显示,默认为 true" } } }, "matches": { "type": "array", "description": "该功能下可响应的命令集,支持 6 种类型,由 matches 的类型或 matches.type 决定。", "items": { "oneOf": [ { "type": "string", "description": "简单字符串匹配" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "description": "类型", "enum": [ "text", "key", "regex", "file", "image", "window", "editor" ] }, "name": { "type": "string", "description": "匹配名称" }, "regex": { "type": "string", "description": "匹配正则表达式" }, "title": { "type": "string", "description": "匹配标题" }, "text": { "type": "string", "description": "匹配文本" }, "minLength": { "type": "number", "description": "最小匹配长度", "minimum": 1 }, "maxLength": { "type": "number", "description": "最大匹配长度", "minimum": 1, "maximum": 10000 }, "key": { "type": "string", "description": "匹配键值" }, "minCount": { "type": "number", "description": "最小匹配数量", "minimum": 1 }, "maxCount": { "type": "number", "description": "最大匹配数量", "minimum": 1, "maximum": 10000 }, "filterFileType": { "type": "string", "description": "文件类型", "enum": [ "file", "directory" ] }, "filterExtensions": { "type": "array", "description": "文件扩展名", "items": { "type": "string" } }, "nameRegex": { "type": "string", "description": "匹配名称正则" }, "titleRegex": { "type": "string", "description": "匹配标题正则" }, "attrRegex": { "type": "object", "description": "匹配属性正则", "properties": { "name": { "type": "string", "description": "属性名称" }, "value": { "type": "string", "description": "属性值正则表达式" } } }, "extensions": { "type": "array", "description": "文件扩展名", "items": { "type": "string" } }, "fadTypes": { "type": "array", "description": "FocusAny 数据类型", "items": { "type": "string" } } }, "additionalProperties": false } ] } } } } }, "mcp": { "type": "object", "description": "MCP 配置,描述了插件应用在 MCP 中的相关信息。", "properties": { "tools": { "type": "array", "description": "插件应用提供的工具列表,一个插件应用可以提供多个工具。", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "工具名称,此为必选项,且插件应用内不可重复。建议使用小写+点号分隔,例如 'list.plugins'。" }, "description": { "type": "string", "description": "工具描述,简要说明工具的用途。" }, "inputSchema": { "type": "object", "description": "工具输入参数的 JSON Schema。必须符合 JSON Schema Draft 7 标准。", "properties": { "type": { "type": "string", "enum": [ "object" ], "description": "输入参数必须是对象类型。" }, "properties": { "type": "object", "description": "对象的属性定义。每个属性名对应一个参数。", "additionalProperties": { "type": "object", "description": "参数的 JSON Schema 定义。例如 {\"type\":\"string\",\"description\":\"...\"}", "properties": { "type": { "type": "string", "description": "参数类型,例如 string、number、boolean、array、object 等。", "enum": [ "string", "number", "object", "array", "boolean" ] }, "description": { "type": "string", "description": "参数描述,简要说明参数的用途。" }, "examples": { "type": "array", "description": "参数示例值。", "items": { "type": [ "string", "number", "object", "array", "boolean" ] } } } } }, "required": { "type": "array", "description": "必填参数列表。", "items": { "type": "string" } } }, "required": [ "type", "properties" ] } }, "required": [ "name", "description", "inputSchema" ] } } }, "required": [ "tools" ] }, "development": { "type": "object", "properties": { "env": { "type": "string", "description": "开发环境,prod 表示生产环境,dev 表示开发环境,prod 环境会忽略 development 的所有配置。", "enum": [ "prod", "dev" ] }, "main": { "type": "string", "description": "dev环境 入口文件,通常为开发环境如 http://localhost:8080" }, "mainView": { "type": "string", "description": "dev环境 快捷面板/智能视图快速面板入口文件,通常为开发环境如 http://localhost:8080" }, "showDevTools": { "type": "boolean", "description": "dev环境 是否在插件加载完成后显示开发者窗口" }, "showCodeDevTools": { "type": "boolean", "description": "dev环境 是否在code执行完成后显示开发者窗口" }, "keepCodeDevTools": { "type": "boolean", "description": "dev环境 插件是否在code执行完成后保留开发者窗口" }, "showViewDevTools": { "type": "boolean", "description": "dev环境 是否在快捷面板/智能视图渲染view时显示开发者窗口" }, "releaseDoc": { "type": "string", "description": "更新日志文档,参照插件选择根目录,默认为 release.md,使用 markdown 格式,格式为【## x.x.x 功能特性[换行][换行]更新内容详情】使用 --- 分割多个。" }, "contentDoc": { "type": "string", "description": "插件内容文档,参照插件选择根目录,默认为 content.md,使用 markdown 格式。" }, "previewDoc": { "type": "string", "description": "插件预览文档,参照插件选择根目录,默认为 preview.md,使用 markdown 格式,每行一个图片链接。" } } }, "permissions": { "type": "array", "description": "插件权限", "items": { "type": "string", "enum": [ "ClipboardManage", "Api", "File" ] } }, "setting": { "type": "object", "properties": { "autoDetach": { "type": "boolean", "description": "是否默认分离模式打开,默认为 false" }, "detachPosition": { "type": "string", "description": "分离模式默认位置,默认为 center", "enum": [ "center", "left-top", "right-top", "left-bottom", "right-bottom" ] }, "detachAlwaysOnTop": { "type": "boolean", "description": "分离模式默认是否置顶,默认为 false" }, "height": { "type": "string", "description": "窗口高度,支持 数字 或 百分比,设置后窗口大小将默认为分离模式,默认为 600" }, "width": { "type": "string", "description": "窗口宽度,支持 数字 或 百分比,设置后窗口大小将默认为分离模式,默认为 800" }, "heightView": { "type": "integer", "description": "快速面板高度,单位为像素,默认为 100" }, "singleton": { "type": "boolean", "description": "是否只允许打开一个窗口,默认为 true" }, "zoom": { "type": "number", "description": "窗口缩放比例,100表示原始大小,默认为 100" }, "darkModeSupport": { "type": "boolean", "description": "是否支持暗黑模式,默认为 false" }, "httpEntry": { "type": "boolean", "description": "是否使用 http 协议入口,默认为 false" }, "remoteWebCacheEnable": { "type": "boolean", "description": "是否启用远程Web缓存,默认为 false" }, "moreMenu": { "type": "array", "description": "分离模式更多菜单,默认为空", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "菜单标识" }, "title": { "type": "string", "description": "菜单标题" } }, "required": [ "name", "title" ] } }, "preloadBase": { "type": "string", "description": "基础预加载文件,普通插件应用不需要设置" }, "nodeIntegration": { "type": "boolean", "description": "是否启用 nodejs,普通应用不需要设置" } } } } } ================================================ FILE: sdk/electron-browser-window.d.ts ================================================ declare module BrowserWindow { interface WebPreferences { devTools?: boolean; preload?: string; zoomFactor: number; [key: string]: any; } interface InitOptions { width?: number; height?: number; webPreferences: WebPreferences; show?: boolean; title?: string; x?: number; y?: number; center?: boolean; resizable?: boolean; fullscreen?: boolean; fullscreenable?: boolean; skipTaskbar?: true; closable?: boolean; frame?: boolean; alwayOnTop?: boolean; [key: string]: any; } interface NativeImage { toPng: (options?: {scaleFator?: number}) => Uint8Array; toJPEG: (options?: {quality?: number}) => Uint8Array; isEmpty: () => boolean; [key: string]: any; } interface PrinterSync { description: string; displayName: string; isDefault: boolean; status: number; options?: { "printer-location"?: string; "printer-make-and-model"?: string; system_driverinfo?: string; }; } type WebRTCIPHandlingPolicy = | "default" | "default_public_interface_only" | "default_public_and_private_interfaces" | "disable_non_proxied_udp"; interface WebContents { id: number; capturePage: () => Promise; closeDevTools: () => void; copy: () => void; copyImageAt: (x: number, y: number) => void; cut: () => void; /** * @deprecated */ decrementCapturerCount: () => any; delete: () => void; disableDeviceEmulation: () => void; enableDeviceEmulation: () => void; executeJavaScript: (code: string, userGesture?: boolean) => Promise; findInPage: ( text: string, options?: { forward?: boolean; findNext?: boolean; matchCase?: boolean; } ) => number; focus: () => void; getBackgroundThrottling: () => boolean; getFrameRate: () => number; getOSProcessId: () => number; getPrinters: () => PrinterSync[]; getProcessId: () => number; getUserAgent: () => string; getWebRTCIPHandlingPolicy: () => WebRTCIPHandlingPolicy; getZoomFactor: () => number; /** * @deprecated */ incrementCapturerCount: () => any; insertCSS: ( css: string, options?: { /** * @default 'author' */ cssOrigin?: "user" | "author"; } ) => Promise; insertText: (text: string) => Promise; invalidate: () => void; isAudioMuted: () => boolean; isBeingCaptured: () => boolean; isCrashed: () => boolean; isCurrentlyAudible: () => boolean; isDestroyed: () => boolean; isDevToolsFocused: () => boolean; isDevToolsOpened: () => boolean; isFocused: () => boolean; isLoading: () => boolean; isLoadingMainFrame: () => boolean; isOffscreen: () => boolean; isPainting: () => void; isWaitingForResponse: () => boolean; openDevTools: (options?: { mode: "left" | "right" | "bottom" | "undocked" | "detach"; activate?: boolean; title?: string; }) => void; paste: () => void; pasteAndMatchStyle: () => void; print: (options?: Record, callback?: (success: boolean, errorType?: string) => void) => void; printToPDF: (options: Record) => Promise; redo: () => void; removeInsertedCSS: (key: string) => Promise; replace: (text: string) => void; replaceMisspelling: (text: string) => void; savePage: (fullPath: string, saveType: "HTMLOnly" | "HTMLComplete" | "MHTML") => Promise; selectAll: () => void; sendInputEvent: (e: any) => void; setAudioMuted: (muted: boolean) => void; setBackgroundThrottling: (allowed: boolean) => void; setFrameRate: (fps: number) => void; setIgnoreMenuShortcuts: (ignore: boolean) => void; setUserAgent: (userAgent: string) => void; setWebRTCIPHandlingPolicy: (policy: WebRTCIPHandlingPolicy) => void; setZoomFactor: (factor: number) => void; startPainting: () => void; stopFindInPage: (action: "clearSelection" | "keepSelection" | "activateSelection") => void; stopPainting: () => void; takeHeapSnapshot: (filePath: string) => Promise; toggleDevTools: () => void; undo: () => void; unselect: () => void; [key: string]: any; } interface Rectangle { x: number; y: number; width: number; height: number; } interface WindowInstance { id: number; webContents: WebContents; show: () => void; hide: () => void; destory: () => void; close: () => void; isFocused: () => boolean; isDestroyed: () => boolean; setResizable: (resizable: boolean) => void; setSize: (width: number, height: number) => void; getSize: () => [width: number, height: number]; isVisible: () => boolean; maximize: () => void; unmaximize: () => void; isMaximized: () => void; minimize: () => void; restore: () => void; isMinimized: () => boolean; setFullScreen: (flag: boolean) => void; isFullScreen: () => boolean; isNormal: () => boolean; setAspectRatio: (aspectiRotio: number) => void; setBackgroundColor: (backgroundColor: string) => void; getBounds: () => Rectangle; getBackgroundColor: () => string; setContentBounds: (bounds: Rectangle) => void; getContentBounds: () => Rectangle; getNormalBounds: () => Rectangle; setEnabled: (enable: boolean) => void; isEnabled: () => boolean; setContentSize: (width: number, height: number) => void; getContentSize: () => [width: number, height: number]; setMinimumSize: (width: number, height: number) => void; getMinimumSize: () => [width: number, height: number]; setMaximumSize: (width: number, height: number) => void; getMaximumSize: () => [width: number, height: number]; isResizable: () => boolean; setFullScreenable: (fullscreenable: boolean) => void; isFullScreenable: () => boolean; setClosable: (closable: boolean) => void; isClosable: () => boolean; setAlwaysOnTop: (flag: boolean) => void; isAlwaysOnTop: () => boolean; moveTop: () => void; setPosition: (x: number, y: number) => void; getPosition: () => [x: number, y: number]; setTitle: (title: string) => void; getTitle: () => string; flashFrame: (flag: boolean) => void; setKiosk: (flag: boolean) => void; isKiosk: () => boolean; focusOnWebView: () => void; blurWebView: () => void; capturePage: ( rect?: Rectangle, options?: { stayHidden?: boolean; stayAwake?: boolean; } ) => Promise; reload: () => void; [key: string]: any; } } ================================================ FILE: sdk/electron.d.ts ================================================ declare module "electron" { type ClipboardType = "selection" | "clipboard"; module clipboard { function availableFormats(type?: ClipboardType): void; function clear(type?: ClipboardType): void; function has(fmt: string, type?: ClipboardType): boolean; function read(fmt: string): string; function readBookmark(): { title: string; url: string; }; function readBuffer(fmt: string): Uint8Array; function readHTML(type?: ClipboardType): string; function readImage(type?: ClipboardType): BrowserWindow.NativeImage; function readRTF(type?: ClipboardType): string; function readText(type?: ClipboardType): string; function write( data: { text?: string; html?: string; image?: BrowserWindow.NativeImage; rtf?: string; bookmark?: string; }, type?: ClipboardType ): void; function writeBookmark(title: string, url: string, type?: ClipboardType): void; function writeBuffer(fmt: string, buffer: Uint8Array, type?: ClipboardType): void; function writeHTML(markup: string, type?: ClipboardType): void; function writeImage(img: BrowserWindow.NativeImage, type?: ClipboardType): void; function writeRTF(text: string, type?: ClipboardType): void; function writeText(text: string, type?: ClipboardType): void; } interface UIpcSendEventInit { senderId: number; } type UIpcSendEventListener = (event: UIpcSendEventInit, ...args: T) => void; module ipcRenderer { function on(channel: string, listener: UIpcSendEventListener): void; function once(channel: string, listener: UIpcSendEventListener): void; function off(channel: string, listener: UIpcSendEventListener): void; function sendTo(id: number, channel: string, ...args: T): void; } module contextBridge {} module webFrame {} module shell {} module nativeImage { type NativeImage = BrowserWindow.NativeImage; function createEmpty(): NativeImage; function createFromPath(path: string): NativeImage; function createFromBitmap( buffer: Uint8Array, options: { width: number; height: number; scaleFator?: number; } ): NativeImage; function createFromBuffer( buffer: Uint8Array, options?: { width?: number; height?: number; scaleFator?: number; } ): NativeImage; function createFromDataURL(dataURL: string): NativeImage; } } ================================================ FILE: sdk/focusany-shim.d.ts ================================================ export interface FocusAnyShimType { init(): void; } export declare const FocusAnyShim: FocusAnyShimType; ================================================ FILE: sdk/focusany-shim.ts ================================================ /// const FocusAnyShim = { init() { if (window["focusany"]) { return; } let hooks: Record = { onLog: (label: string, data?: any) => { console.log(`FocusAny Log: ${label}`, data || ""); } } const focusanySupport = { onLog(callback: (label: string, data?: any) => void): void { hooks.onLog = callback; }, onPluginReady( callback: (data: { actionName: string; actionMatch: any | null; actionMatchFiles: FileItem[]; requestId: string; reenter: boolean; isView: boolean; }) => void ): void { const callbackWrapper = () => { callback({ actionName: "", actionMatch: null, actionMatchFiles: [], requestId: "", reenter: false, isView: false, }); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callbackWrapper); } else { callbackWrapper(); } }, copyText(text: string): boolean { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text); return true; } else { console.error("FocusAny Shim: copyText() requires clipboard permission in web environment"); return false; } }, isMacOs(): boolean { return navigator.platform.toLowerCase().includes("mac"); }, isWindows(): boolean { return navigator.platform.toLowerCase().includes("win"); }, isLinux(): boolean { return navigator.platform.toLowerCase().includes("linux"); }, showNotification(body: string, clickActionName?: string): void { focusanySupport.showToast(body, { duration: 5000, status: "info", }); }, showToast(body: string, options?: { duration?: number; status?: "info" | "success" | "error" }): void { options = options || {}; const duration = typeof options.duration === "number" && options.duration >= 0 ? options.duration : 3000; const status = ["info", "success", "error"].includes(options.status as string) ? options.status : "info"; // 创建SVG图标函数 const createSvgIcon = (type: string): string => { const svgBase = 'xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"'; switch (type) { case "info": return ``; case "success": return ``; case "error": return ``; default: return ``; } }; const statusStyles = { info: {background: "#1890ff", color: "#ffffff", icon: createSvgIcon("info")}, success: {background: "#52c41a", color: "#ffffff", icon: createSvgIcon("success")}, error: {background: "#ff4d4f", color: "#ffffff", icon: createSvgIcon("error")}, }; const currentStyle = statusStyles[status as keyof typeof statusStyles]; let container = document.getElementById("focusany-shim-toast-container"); if (!container) { container = document.createElement("div"); container.id = "focusany-shim-toast-container"; container.style.cssText = ` position: fixed !important; top: 20px !important; right: 20px !important; z-index: 999999 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; pointer-events: none !important; max-width: 350px !important; width: auto !important; `; document.body.appendChild(container); } // 创建通知元素 const notification = document.createElement("div"); notification.style.cssText = ` background: ${currentStyle.background} !important; color: ${currentStyle.color} !important; padding: 12px 30px 12px 16px !important; margin-bottom: 10px !important; border-radius: 6px !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; font-size: 14px !important; line-height: 1.4 !important; width: 100% !important; max-width: 320px !important; word-wrap: break-word !important; opacity: 0 !important; transform: translateX(350px) !important; transition: all 0.3s ease !important; pointer-events: auto !important; cursor:default !important; border: none !important; outline: none !important; text-decoration: none !important; box-sizing: border-box !important; display: block !important; position: relative !important; min-height: 20px !important; `; // 创建内容容器 const content = document.createElement("div"); content.style.cssText = ` display: flex !important; align-items: center !important; gap: 8px !important; `; // 添加状态图标 const iconSpan = document.createElement("span"); iconSpan.innerHTML = currentStyle.icon; iconSpan.style.cssText = ` font-size: 16px !important; line-height: 1 !important; flex-shrink: 0 !important; padding: 4px 6px !important; border-radius: 4px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; `; // 添加文本内容 const textSpan = document.createElement("span"); textSpan.textContent = body; textSpan.style.cssText = ` flex: 1 !important; `; content.appendChild(iconSpan); content.appendChild(textSpan); notification.appendChild(content); const closeButton = document.createElement("span"); closeButton.textContent = "×"; closeButton.style.cssText = ` position: absolute !important; top: 50% !important; right: 8px !important; transform: translateY(-50%) !important; font-size: 16px !important; font-weight: bold !important; cursor: pointer !important; color: rgba(255, 255, 255, 0.8) !important; line-height: 1 !important; width: 20px !important; height: 20px !important; text-align: center !important; display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 50% !important; background: rgba(255, 255, 255, 0.1) !important; transition: all 0.2s ease !important; backdrop-filter: blur(4px) !important; `; closeButton.addEventListener("mouseenter", () => { closeButton.style.color = "#ffffff !important"; closeButton.style.backgroundColor = "rgba(255, 255, 255, 0.2) !important"; closeButton.style.transform = "translateY(-50%) scale(1.1) !important"; }); closeButton.addEventListener("mouseleave", () => { closeButton.style.color = "rgba(255, 255, 255, 0.8) !important"; closeButton.style.backgroundColor = "rgba(255, 255, 255, 0.1) !important"; closeButton.style.transform = "translateY(-50%) scale(1) !important"; }); closeButton.addEventListener("click", e => { e.stopPropagation(); removeNotification(); }); notification.appendChild(closeButton); container.appendChild(notification); setTimeout(() => { notification.style.setProperty("opacity", "1", "important"); notification.style.setProperty("transform", "translateX(0)", "important"); }, 10); const removeNotification = () => { notification.style.setProperty("opacity", "0", "important"); notification.style.setProperty("transform", "translateX(350px)", "important"); setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } // 如果容器为空,移除容器 if (container && container.children.length === 0) { if (container.parentNode) { container.parentNode.removeChild(container); } } }, 300); }; // 使用配置的 duration 时间自动移除 if (duration > 0) { setTimeout(removeNotification, duration); } }, // use localStorage with prefix db:xxx to store data db: { put(doc: DbDoc): DbReturn { const key = `db:${doc._id}`; try { localStorage.setItem(key, JSON.stringify(doc)); return { ok: true, id: doc._id, rev: doc._rev || focusanySupport.util.randomString(16), }; } catch (e) { console.error("FocusAny Shim: db.put() failed:", e); return {ok: false, id: doc._id, rev: ""}; } }, get>(id: string): DbDoc | null { const key = `db:${id}`; const value = localStorage.getItem(key); if (value) { try { return JSON.parse(value) as DbDoc; } catch (e) { console.error("FocusAny Shim: db.get() failed:", e); return null; } } return null; }, remove(doc: string | DbDoc): DbReturn { const id = typeof doc === "string" ? doc : doc._id; const key = `db:${id}`; try { localStorage.removeItem(key); return {ok: true, id, rev: ""}; } catch (e) { console.error("FocusAny Shim: db.remove() failed:", e); return {ok: false, id, rev: ""}; } }, bulkDocs(docs: DbDoc[]): DbReturn[] { const results: DbReturn[] = []; for (const doc of docs) { const result = this.put(doc); results.push(result); } return results; }, allDocs>(key?: string): DbDoc[] { const results: DbDoc[] = []; const prefix = key ? `db:${key}` : "db:"; for (const item of Object.keys(localStorage)) { if (item.startsWith(prefix)) { const value = localStorage.getItem(item); if (value) { try { results.push(JSON.parse(value) as DbDoc); } catch (e) { console.error("FocusAny Shim: db.allDocs() failed:", e); } } } } return results; }, postAttachment(docId: string, attachment: Uint8Array, type: string): DbReturn { const key = `dbAttachment:${docId}`; try { const existing = localStorage.getItem(key); const attachments = existing ? JSON.parse(existing) : {}; attachments[type] = focusanySupport.util.bufferToBase64(attachment); localStorage.setItem(key, JSON.stringify(attachments)); return {ok: true, id: docId, rev: ""}; } catch (e) { console.error("FocusAny Shim: db.postAttachment() failed:", e); return {ok: false, id: docId, rev: ""}; } }, getAttachment(docId: string): Uint8Array | null { const key = `dbAttachment:${docId}`; const value = localStorage.getItem(key); if (value) { try { const attachments = JSON.parse(value); const firstKey = Object.keys(attachments)[0]; if (firstKey) { return focusanySupport.util.base64ToBuffer(attachments[firstKey]); } } catch (e) { console.error("FocusAny Shim: db.getAttachment() failed:", e); } } return null; }, getAttachmentType(docId: string): string | null { const key = `dbAttachment:${docId}`; const value = localStorage.getItem(key); if (value) { try { const attachments = JSON.parse(value); const firstKey = Object.keys(attachments)[0]; return firstKey || null; } catch (e) { console.error("FocusAny Shim: db.getAttachmentType() failed:", e); } } return null; }, }, dbStorage: { setItem(key: string, value: any): void { localStorage.setItem(key, JSON.stringify(value)); }, getItem(key: string): T { const value = localStorage.getItem(key); return value ? JSON.parse(value) : null as any; }, removeItem(key: string): void { localStorage.removeItem(key); }, }, util: { randomString(length: number): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }, bufferToBase64(buffer: Uint8Array): string { return btoa(String.fromCharCode.apply(null, Array.from(buffer))); }, base64ToBuffer(base64: string): Uint8Array { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; }, datetimeString(): string { return new Date().toISOString(); }, base64Encode(data: any): string { return btoa(JSON.stringify(data)); }, base64Decode(data: string): any { return JSON.parse(atob(data)); }, md5(data: string): string { console.error( "FocusAny Shim: util.md5() is not supported in web environment, use crypto.subtle instead" ); return ""; }, save(filename: string, data: string | Uint8Array, option?: { isBase64?: boolean }): boolean { // 使用浏览器下载功能 try { const blob = new Blob([data as any], {type: "application/octet-stream"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return true; } catch (e) { console.error("FocusAny Shim: util.save() failed:", e); return false; } }, }, // Additional unimplemented methods with mock data onPluginExit(callback: Function): void { // Mock: do nothing }, onPluginEvent(event: PluginEvent, callback: (data: any) => void): void { // Mock: do nothing }, offPluginEvent(event: PluginEvent, callback: (data: any) => void): void { // Mock: do nothing }, offPluginEventAll(event: PluginEvent): void { // Mock: do nothing }, onMoreMenuClick(callback: (data: { name: string }) => void): void { // Mock: do nothing }, registerHotkey(key: string | string[] | HotkeyQuickType | HotkeyType | HotkeyType[], callback: () => void): void { // Mock: do nothing }, unregisterHotkeyAll(): void { // Mock: do nothing }, isMainWindowShown(): boolean { return true; // Mock: always shown }, hideMainWindow(): void { // Mock: do nothing }, showMainWindow(): void { // Mock: do nothing }, isFastPanelWindowShown(): boolean { return false; // Mock: not shown }, showFastPanelWindow(): void { // Mock: do nothing }, hideFastPanelWindow(): void { // Mock: do nothing }, setExpendHeight(height: number): void { // Mock: do nothing }, setSubInput(onChange: (keywords: string) => void, placeholder?: string, isFocus?: boolean, isVisible?: boolean): void { // Mock: do nothing }, removeSubInput(): void { // Mock: do nothing }, setSubInputValue(value: string): void { // Mock: do nothing }, subInputBlur(): void { // Mock: do nothing }, getPluginRoot(): string { return "/mock/plugin/root"; // Mock path }, getPluginConfig(): { name: string; title: string; version: string; logo: string; } | null { return { name: "mock-plugin", title: "Mock Plugin", version: "1.0.0", logo: "" }; }, getPluginInfo(): { nodeIntegration: boolean; preloadBase: string; preload: string; main: string; mainView: string; width: number; height: number; autoDetach: boolean; singleton: boolean; zoom: number; } { return { nodeIntegration: false, preloadBase: "", preload: "", main: "", mainView: "", width: 800, height: 600, autoDetach: false, singleton: false, zoom: 1 }; }, getPluginEnv(): "dev" | "prod" { return "dev"; }, getQuery(requestId: string): SearchQuery { return { keywords: "", currentFiles: [], currentImage: "", currentText: "" }; }, createBrowserWindow(url: string, options: any, callback?: () => void): any { // Mock: open in new tab window.open(url, "_blank"); if (callback) callback(); return { close: () => { } }; }, outPlugin(): void { // Mock: do nothing }, isDarkColors(): boolean { return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; }, showUserLogin(): void { // Mock: do nothing }, getUser(): { isLogin: boolean; avatar: string; nickname: string; vipFlag: string; deviceCode: string; openId: string; } { return { isLogin: false, avatar: "", nickname: "Mock User", vipFlag: "", deviceCode: "mock-device", openId: "" }; }, getUserAccessToken(): Promise<{ token: string; expireAt: number; }> { return Promise.resolve({ token: "mock-token", expireAt: Date.now() + 3600000 }); }, listGoods(query?: { ids?: string[] }): Promise<{ id: string; title: string; cover: string; priceType: "fixed" | "dynamic"; fixedPrice: string; description: string; }[]> { return Promise.resolve([]); }, openGoodsPayment(options: { goodsId: string; price?: string; outOrderId?: string; outParam?: string; }): Promise<{ paySuccess: boolean; }> { return Promise.resolve({paySuccess: false}); }, queryGoodsOrders(options: { goodsId?: string; page?: number; pageSize?: number; }): Promise<{ page: number; total: number; records: { id: string; goodsId: string; status: "Paid" | "Unpaid"; }[]; }> { return Promise.resolve({ page: 1, total: 0, records: [] }); }, apiPost(url: string, body: any, option: {}): Promise { return Promise.resolve({ code: 200, msg: "Mock response", data: null }); }, setAction(action: PluginAction | PluginAction[]): void { // Mock: do nothing }, removeAction(name: string): void { // Mock: do nothing }, getActions(names?: string[]): PluginAction[] { return []; }, redirect(keywordsOrAction: string | string[], query?: SearchQuery): void { // Mock: do nothing }, showMessageBox(message: string, options: { title?: string; yes?: string; no?: string; }): boolean { return confirm(message); // Mock: use browser confirm }, showOpenDialog(options: { title?: string; defaultPath?: string; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; properties?: any[]; message?: string; securityScopedBookmarks?: boolean; }): string[] | undefined { // Mock: return empty array return []; }, showSaveDialog(options: { title?: string; defaultPath?: string; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; message?: string; nameFieldLabel?: string; showsTagField?: string; properties?: any[]; securityScopedBookmarks?: boolean; }): string | undefined { // Mock: return null return undefined; }, screenCapture(callback: (imgBase64: string) => void): void { // Mock: do nothing }, getNativeId(): string { return "mock-native-id"; }, getAppVersion(): string { return "1.0.0"; }, getPath(name: "home" | "appData" | "userData" | "temp" | "exe" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "logs"): string { return `/mock/${name}`; }, getFileIcon(path: string): string { return ""; // Mock: empty icon }, copyFile(file: string | string[]): boolean { return false; // Mock: not supported }, copyImage(image: string): boolean { return false; // Mock: not supported }, getClipboardText(): string { return ""; // Mock: empty }, getClipboardImage(): string { return ""; // Mock: empty }, getClipboardFiles(): FileItem[] { return []; }, listClipboardItems(option?: { limit?: number }): Promise<{ type: "file" | "image" | "text"; timestamp: number; files?: FileItem[]; image?: string; text?: string; }[]> { return Promise.resolve([]); }, deleteClipboardItem(timestamp: number): Promise { return Promise.resolve(); }, clearClipboardItems(): Promise { return Promise.resolve(); }, shellOpenPath(fullPath: string): void { // Mock: do nothing }, shellShowItemInFolder(fullPath: string): void { // Mock: do nothing }, shellOpenExternal(url: string): void { window.open(url, "_blank"); }, shellBeep(): void { // Mock: do nothing }, simulate: { keyboardTap(key: string, modifiers: ("ctrl" | "shift" | "command" | "option" | "alt")[]): Promise { return Promise.resolve(); }, typeString(text: string): Promise { return Promise.resolve(); }, mouseToggle(type: "down" | "up", button: "left" | "right" | "middle"): Promise { return Promise.resolve(); }, mouseMove(x: number, y: number): Promise { return Promise.resolve(); }, mouseClick(button: "left" | "right" | "middle", double?: boolean): Promise { return Promise.resolve(); }, }, getCursorScreenPoint(): { x: number; y: number } { return {x: 0, y: 0}; // Mock: center }, getDisplayNearestPoint(point: { x: number; y: number }): any { return { id: 1, bounds: {x: 0, y: 0, width: 1920, height: 1080}, workArea: {x: 0, y: 0, width: 1920, height: 1040}, scaleFactor: 1 }; }, getPlatformArch(): "x86" | "arm64" | null { return "x86"; // Mock }, sendBackendEvent(event: string, data?: any, option?: { timeout: number; }): Promise { return Promise.resolve(null); }, registerCallPage(type: string, callback: (resolve: (data: any) => void, reject: (error: string) => void, data: any) => void, option?: { timeout?: number; }): void { // Mock: do nothing }, callPage(type: string, data?: any, option?: CallPageOption): Promise { return Promise.resolve(null); }, setRemoteWebRuntime(info: { userAgent: string; urlMap: Record; types: string[]; domains: string[]; blocks: string[]; }): Promise { return Promise.resolve(undefined); }, llmListModels(): Promise<{ providerId: string; providerLogo: string; providerTitle: string; modelId: string; modelName: string; }[]> { return Promise.resolve([ { providerId: "openai", providerLogo: "https://cdn.openai.com/chatgpt/images/chatgpt-logo.png", providerTitle: "OpenAI", modelId: "gpt-4", modelName: "GPT-4" }, { providerId: "anthropic", providerLogo: "https://www.anthropic.com/images/anthropic-logo.png", providerTitle: "Anthropic", modelId: "claude-3-opus", modelName: "Claude 3 Opus" }, ]); }, llmChat(callInfo: { providerId: string; modelId: string; message: string }): Promise> { return Promise.resolve({ code: 200, msg: "Mock response", data: {message: "Mock LLM response"} }); }, logInfo(label: string, data?: any): void { console.log(`FocusAny Log Info: ${label}`, data || ""); }, logError(label: string, data?: any): void { console.error(`FocusAny Log Error: ${label}`, data || ""); }, logPath(): Promise { return Promise.resolve("/mock/log/path"); }, logShow(): void { // Mock: do nothing }, addLaunch(keyword: string, name: string, hotkey: HotkeyType): Promise { return Promise.resolve(); }, removeLaunch(keyword: string): void { // Mock: do nothing }, activateLatestWindow(): Promise { return Promise.resolve(); }, file: { exists(path: string): Promise { return Promise.resolve(false); }, read(path: string): Promise { return Promise.resolve(""); }, write(path: string, data: string): Promise { return Promise.resolve(); }, remove(path: string): Promise { return Promise.resolve(); }, ext(path: string): Promise { return Promise.resolve(""); }, writeTemp(ext: string, data: string | Uint8Array, option?: { isBase64?: boolean; }): Promise { return Promise.resolve(`/mock/temp/file.${ext}`); }, }, fad: { read(type: string, path: string): Promise { return Promise.resolve(null); }, write(type: string, path: string, data: any): Promise { return Promise.resolve(); }, }, view: { setHeight(height: number): void { // Mock: do nothing }, getHeight(): Promise { return Promise.resolve(400); }, }, detach: { setTitle(title: string): void { // Mock: do nothing }, setOperates(operates: { name: string; title: string; click: () => void; }[]): void { // Mock: do nothing }, setPosition(position: "center" | "right-bottom" | "left-top" | "right-top" | "left-bottom"): void { // Mock: do nothing }, setAlwaysOnTop(alwaysOnTop: boolean): void { // Mock: do nothing }, setSize(width: number, height: number): void { // Mock: do nothing }, }, } as FocusAnyApi // 创建一个递归的 Proxy 来处理任意深度的属性访问 function createErrorProxy(path: string = "focusany", supportObj?: any): any { return new Proxy(() => { }, { get(target, prop) { const currentPath = `${path}.${String(prop)}`; // 如果是根级别且在支持对象中存在,直接返回 if (path === "focusany" && supportObj && prop in supportObj) { const result = supportObj[prop]; // console.log('FocusAny Shim: Accessing supported property:', {currentPath, result}); return new Proxy(result, { get(t, p) { // console.log('FocusAny Shim: Accessing supported sub-property:', {currentPath: `${currentPath}.${String(p)}`}); const value = (t as any)[p]; if (typeof value === "function") { return function (...args: any[]) { return value.apply(t, args); }; } return value; }, apply(t, thisArg, args) { console.log('FocusAny Shim: Calling supported function:', { t, currentPath, thisArg, args }); const ret = (t as any).apply(thisArg, args); const pcs = currentPath.split("."); const name = pcs[pcs.length - 1]; hooks.onLog && hooks.onLog(name, args.map(a => { if (a instanceof Function) return 'function(){}'; return a; })); if (ret instanceof Promise) { return ret.then((data: any) => { hooks.onLog && hooks.onLog(`${name}.result`, data); return data; }); } else { hooks.onLog && hooks.onLog(`${name}.result`, ret); } return ret; } }); } // 对于其他属性,返回一个新的 Proxy(不支持任何属性) return createErrorProxy(currentPath, null); }, apply(target, thisArg, argumentsList) { console.error(`FocusAny Shim: ${path}() is not supported in web environment`); }, }); } // 创建 focusany 对象 const focusany = createErrorProxy("focusany", focusanySupport); // @ts-ignore window["focusany"] = focusany; }, }; // 自动初始化:在浏览器环境中自动调用 init() if (typeof window !== "undefined") { FocusAnyShim.init(); } ================================================ FILE: sdk/focusany.d.ts ================================================ /// /// declare interface Window { focusany: FocusAnyApi; } type DbDoc> = { _id: string; _rev?: string; } & T; interface DbReturn { id: string; rev?: string; ok?: boolean; error?: boolean; name?: string; message?: string; } declare type BaseResult = { code: number; msg: string; data?: T; }; declare type PlatformType = "win" | "osx" | "linux"; declare type EditionType = "open" | "pro"; declare type PluginEvent = "ClipboardChange" | "UserChange"; declare type HotkeyModifierType = "Control" | "Option" | "Command" | "Ctrl" | "Alt" | "Win" | "Meta" | "Shift"; declare type HotkeyType = { key: string; modifiers: HotkeyModifierType[] }; declare type HotkeyQuickType = "save"; declare type ActionMatch = | ActionMatchText | ActionMatchKey | ActionMatchRegex | ActionMatchFile | ActionMatchImage | ActionMatchWindow | ActionMatchEditor; declare enum ActionMatchTypeEnum { TEXT = "text", KEY = "key", REGEX = "regex", IMAGE = "image", FILE = "file", WINDOW = "window", EDITOR = "editor", } type SearchQuery = { keywords: string; currentFiles?: FileItem[]; currentImage?: string; currentText?: string; }; type FileItem = { name: string; isDirectory: boolean; isFile: boolean; path: string; fileExt: string; }; type ActionCodeSetting = { type: "list", placeholder: string; } type ActionCodeExecuteResultItem = { id: string; icon: string; title: string; description: string; loading?: boolean; // additional data [key: string]: any; } type ActionCodeExecuteResult = { command: "data" | "none" | "error" | "close" | "clear"; // set placeholder when placeholder is set placeholder?: string; // command === data items?: ActionCodeExecuteResultItem[], // command === error error?: string; // additional data [key: string]: any; } declare type ActionMatchBase = { type: ActionMatchTypeEnum; name?: string; }; declare type ActionMatchText = ActionMatchBase & { text: string; minLength: number; maxLength: number; }; declare type ActionMatchKey = ActionMatchBase & { key: string; }; declare type ActionMatchRegex = ActionMatchBase & { regex: string; title: string; minLength: number; maxLength: number; }; declare type ActionMatchFile = ActionMatchBase & { title: string; minCount: number; maxCount: number; filterFileType: "file" | "directory"; filterExtensions: string[]; }; declare type ActionMatchImage = ActionMatchBase & { title: string; }; declare type ActionMatchWindow = ActionMatchBase & { nameRegex: string; titleRegex: string; attrRegex: Record; }; declare type ActionMatchEditor = ActionMatchBase & { extensions: string[]; fadTypes: string[]; }; interface PluginAction { fullName?: string; name: string; title: string; matches: ActionMatch[]; platform?: PlatformType[]; icon?: string; type?: "command" | "web" | "code" | "backend"; } declare type CallPageOption = { // default 10000 ms waitReadyTimeout?: number, // default 60000 ms timeout?: number; // default true, if false the render function will not work showWindow?: boolean; // default true autoClose?: boolean; } interface FocusAnyApi { /** * set log listener * @param callback */ onLog(callback: (label: string, data?: any) => void): void; /** * Plugin application initialization complete callback * @param callback */ onPluginReady( callback: (data: { actionName: string; actionMatch: ActionMatch | null; actionMatchFiles: FileItem[]; requestId: string; reenter: boolean; isView: boolean; type: "action" | "callPage" }) => void ): void; /** * Plugin application exit callback * @param callback */ onPluginExit(callback: Function): void; /** * Plugin event listener * @param event * @param callback */ onPluginEvent(event: PluginEvent, callback: (data: any) => void): void; /** * Plugin event unbind * @param event * @param callback */ offPluginEvent(event: PluginEvent, callback: (data: any) => void): void; /** * Plugin event unbind all * @param event */ offPluginEventAll(event: PluginEvent): void; /** * plugin more menu click * @param callback */ onMoreMenuClick(callback: (data: { name: string }) => void): void; /** * register hotkey * @param key * @param callback */ registerHotkey(key: string | string[] | HotkeyQuickType | HotkeyType | HotkeyType[], callback: () => void): void; /** * unregister all hotkey */ unregisterHotkeyAll(): void; /** * Check if plugin main window is shown */ isMainWindowShown(): boolean; /** * Hide plugin main window */ hideMainWindow(): void; /** * Show plugin main window */ showMainWindow(): void; /** * Check if fast panel window is shown */ isFastPanelWindowShown(): boolean; /** * Show fast panel window */ showFastPanelWindow(): void; /** * Hide fast panel window */ hideFastPanelWindow(): void; /** * Set plugin height * @param height */ setExpendHeight(height: number): void; /** * Set input box listener * @param onChange * @param placeholder * @param isFocus * @param isVisible */ setSubInput( onChange: (keywords: string) => void, placeholder?: string, isFocus?: boolean, isVisible?: boolean ): void; /** * Remove input box listener */ removeSubInput(): void; /** * Set sub input box value * @param value */ setSubInputValue(value: string): void; /** * Sub input box lose focus */ subInputBlur(): void; /** * Get plugin root directory */ getPluginRoot(): string; /** * Get plugin configuration */ getPluginConfig(): { name: string; title: string; version: string; logo: string; } | null; /** * Get plugin information */ getPluginInfo(): { nodeIntegration: boolean; preloadBase: string; preload: string; main: string; mainView: string; width: number; height: number; autoDetach: boolean; singleton: boolean; zoom: number; }; /** * Get plugin environment */ getPluginEnv(): "dev" | "prod"; /** * Get plugin query information * @param requestId */ getQuery(requestId: string): SearchQuery; /** * Create browser window * @param url * @param options * @param callback */ createBrowserWindow( url: string, options: BrowserWindow.InitOptions, callback?: () => void ): BrowserWindow.WindowInstance; /** * Close plugin */ outPlugin(): void; /** * Check if dark theme */ isDarkColors(): boolean; /** * Show user login dialog */ showUserLogin(): void; /** * Get user information */ getUser(): { isLogin: boolean; avatar: string; nickname: string; vipFlag: string; deviceCode: string; openId: string; }; /** * Get user server-side temporary token */ getUserAccessToken(): Promise<{ token: string; expireAt: number; }>; /** * List plugin goods * @param query */ listGoods(query?: { ids?: string[] }): Promise< { id: string; title: string; cover: string; priceType: "fixed" | "dynamic"; fixedPrice: string; description: string; }[] >; /** * Create order and display payment * @param options Order parameters */ openGoodsPayment(options: { /** * Plugin goods ID */ goodsId: string; /** * Plugin goods price, no need to pass for fixed price goods, dynamic price goods need to pass price, e.g. 0.01 */ price?: string; /** * Third-party order ID, string, max length 64 characters */ outOrderId?: string; /** * Parameter data, length not exceeding 200 characters */ outParam?: string; }): Promise<{ /** * Whether payment is successful */ paySuccess: boolean; }>; /** * Query plugin goods orders * @param options */ queryGoodsOrders(options: { /** * Plugin goods ID, optional */ goodsId?: string; /** * Page number, starting from 1, optional */ page?: number; /** * Page size, optional, default is 10 */ pageSize?: number; }): Promise<{ /** * Current page number */ page: number; /** * Total number of orders */ total: number; /** * Order list */ records: { /** * Order ID */ id: string; /** * Goods ID */ goodsId: string; /** * Status: Paid: Paid, Unpaid: Unpaid */ status: "Paid" | "Unpaid"; }[]; }>; /** * Request official API */ apiPost(url: string, body: any, option: {}): Promise; /** * Dynamically set plugin action * @param action */ setAction(action: PluginAction | PluginAction[]): void; /** * Remove plugin action * @param name */ removeAction(name: string): void; /** * Get plugin actions * @param names */ getActions(names?: string[]): PluginAction[]; /** * Open plugin action * @param keywordsOrAction * @param query */ redirect(keywordsOrAction: string | string[], query?: SearchQuery): void; /** * Show toast notification * @param body * @param options */ showToast( body: string, options?: { duration?: number; status?: "info" | "success" | "error"; } ): void; /** * Show notification * @param body * @param clickActionName */ showNotification(body: string, clickActionName?: string): void; /** * Show message box * @param message * @param options * @return true if "yes" is clicked, false if "no" is clicked */ showMessageBox( message: string, options: { title?: string; yes?: string; no?: string; } ): boolean; /** * Show open file dialog * @param options */ showOpenDialog(options: { title?: string; defaultPath?: string; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; properties?: Array< | "openFile" | "openDirectory" | "multiSelections" | "showHiddenFiles" | "createDirectory" | "promptToCreate" | "noResolveAliases" | "treatPackageAsDirectory" | "dontAddToRecent" >; message?: string; securityScopedBookmarks?: boolean; }): string[] | undefined; /** * Show save file dialog * @param options */ showSaveDialog(options: { title?: string; defaultPath?: string; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; message?: string; nameFieldLabel?: string; showsTagField?: string; properties?: Array< | "showHiddenFiles" | "createDirectory" | "treatPackageAsDirectory" | "showOverwriteConfirmation" | "dontAddToRecent" >; securityScopedBookmarks?: boolean; }): string | undefined; /** * Take screenshot * @param callback */ screenCapture(callback: (imgBase64: string) => void): void; /** * Get device ID */ getNativeId(): string; /** * Get software version */ getAppVersion(): string; /** * Get system path * @param name */ getPath( name: | "home" | "appData" | "userData" | "temp" | "exe" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "logs" ): string; /** * Get file icon as Base64 * @param path */ getFileIcon(path: string): string; /** * Copy file to clipboard * @param file */ copyFile(file: string | string[]): boolean; /** * Copy image to clipboard * @param image */ copyImage(image: string): boolean; /** * Copy text to clipboard * @param text */ copyText(text: string): boolean; /** * Get clipboard text */ getClipboardText(): string; /** * Get clipboard image */ getClipboardImage(): string; /** * Get clipboard files */ getClipboardFiles(): FileItem[]; /** * List clipboard history */ listClipboardItems(option?: { limit?: number }): Promise<{ type: "file" | "image" | "text"; timestamp: number; files?: FileItem[]; image?: string; text?: string; }[]>; /** * Delete clipboard item by timestamp * @param timestamp */ deleteClipboardItem(timestamp: number): Promise; /** * Clear clipboard history */ clearClipboardItems(): Promise; /** * Open file with default application * @param fullPath */ shellOpenPath(fullPath: string): void; /** * Show file in file manager * @param fullPath */ shellShowItemInFolder(fullPath: string): void; /** * Open URL with external browser * @param url */ shellOpenExternal(url: string): void; /** * Play system beep sound */ shellBeep(): void; /** * simulate user input */ simulate: { /** * simulate keyboard tap * @param key * @param modifiers */ keyboardTap(key: string, modifiers: ("ctrl" | "shift" | "command" | "option" | "alt")[]): Promise; /** * simulate type string * @param text */ typeString(text: string): Promise; /** * simulate mouse toggle * @param type * @param button */ mouseToggle(type: "down" | "up", button: "left" | "right" | "middle"): Promise; /** * simulate mouse move * @param x * @param y */ mouseMove(x: number, y: number): Promise; /** * simulate mouse click * @param button * @param double */ mouseClick(button: "left" | "right" | "middle", double?: boolean): Promise; }; /** * Get cursor screen position */ getCursorScreenPoint(): { x: number; y: number }; /** * Get display nearest to point * @param point */ getDisplayNearestPoint(point: { x: number; y: number }): any; /** * Check if running on macOS */ isMacOs(): boolean; /** * Check if running on Windows */ isWindows(): boolean; /** * Check if running on Linux */ isLinux(): boolean; /** * Get platform architecture */ getPlatformArch(): "x86" | "arm64" | null; /** * Send backend event * @param event * @param data * @param option */ sendBackendEvent( event: string, data?: any, option?: { timeout: number; } ): Promise; /** * Register backend caller in web * @param type * @param callback * @param option */ registerCallPage( type: string, callback: ( resolve: (data: DataOutput) => void, reject: (error: string) => void, data: DataInput ) => void, option?: { timeout?: number; } ): void; /** * call backend from backend.cjs script * @param type * @param data * @param option */ callPage( type: string, data?: DataInput, option?: CallPageOption ): Promise; /** * set remote web runtime * @param info */ setRemoteWebRuntime(info: { userAgent: string; urlMap: Record; types: string[]; domains: string[]; blocks: string[]; }): Promise; /** * list large language model */ llmListModels(): Promise< { providerId: string; providerLogo: string; providerTitle: string; modelId: string; modelName: string; }[] >; /** * call large language model chat * @param callInfo */ llmChat(callInfo: { providerId: string; modelId: string; message: string }): Promise< BaseResult<{ message: string; }> >; /** * write info log * @param label * @param data */ logInfo(label: string, data?: any): void; /** * write error log * @param label * @param data */ logError(label: string, data?: any): void; /** * get log file path */ logPath(): Promise; /** * show log file */ logShow(): void; /** * add launch * @param keyword * @param name * @param hotkey */ addLaunch( keyword: string, name: string, hotkey: HotkeyType ): Promise; /** * remove launch * @param keyword */ removeLaunch(keyword: string): void; /** * activate latest window */ activateLatestWindow(): Promise; /** * File operations */ file: { /** * Check if file or directory exists * @param path */ exists(path: string): Promise; /** * Read file content * @param path File path * @param format File content format, default is 'string' */ read( path: string, format?: 'string' | 'buffer' | 'base64' ): Promise; /** * Write file content * @param path File path * @param data File content * @param option Write options */ write( path: string, data: string | Uint8Array, option?: { isBase64?: boolean; } ): Promise; /** * Delete file or directory * @param path File or directory path */ remove(path: string): Promise; /** * Get file extension */ ext(path: string): Promise; /** * save file to temp path */ writeTemp( ext: string, data: string | Uint8Array, option?: { isBase64?: boolean; } ): Promise; }; /** * Database operations */ db: { /** * Add document * @param doc */ put(doc: DbDoc): DbReturn; /** * Get document * @param id */ get>(id: string): DbDoc | null; /** * Delete document * @param doc */ remove(doc: string | DbDoc): DbReturn; /** * Bulk add documents * @param docs */ bulkDocs(docs: DbDoc[]): DbReturn[]; /** * Bulk get documents * @param key */ allDocs>(key?: string): DbDoc[]; /** * Save attachment * @param docId * @param attachment * @param type */ postAttachment(docId: string, attachment: Uint8Array, type: string): DbReturn; /** * Get attachment * @param docId */ getAttachment(docId: string): Uint8Array | null; /** * Get attachment type * @param docId */ getAttachmentType(docId: string): string | null; }; /** * Local storage */ dbStorage: { /** * Set storage * @param key * @param value */ setItem(key: string, value: any): void; /** * Get storage * @param key */ getItem(key: string): T; /** * Remove storage * @param key */ removeItem(key: string): void; }; /** * Fast access documents */ fad: { /** * Read fast access document content * @param type * @param path */ read(type: string, path: string): Promise; /** * Write fast access document content * @param type * @param path * @param data */ write(type: string, path: string, data: any): Promise; }; /** * Quick panel view */ view: { /** * Set height of current plugin render area in quick panel * @param height */ setHeight(height: number): void; /** * Get height of current plugin render area in quick panel */ getHeight(): Promise; }; /** * Detached window */ detach: { /** * Set detached window title * @param title */ setTitle(title: string): void; /** * set the detach window actions * @param operates */ setOperates( operates: { name: string; title: string; click: () => void; }[] ): void; /** * Set detached window position * @param position */ setPosition(position: "center" | "right-bottom" | "left-top" | "right-top" | "left-bottom"): void; /** * Set detached window always on top * @param alwaysOnTop */ setAlwaysOnTop(alwaysOnTop: boolean): void; /** * Set detached window size */ setSize(width: number, height: number): void; }; /** * Utilities */ util: { /** * Generate random string * @param length */ randomString(length: number): string; /** * Convert Buffer to Base64 * @param buffer */ bufferToBase64(buffer: Uint8Array): string; /** * Convert Base64 to Buffer */ base64ToBuffer(base64: string): Uint8Array; /** * Get current timestamp string */ datetimeString(): string; /** * Convert data to Base64 * @param data */ base64Encode(data: any): string; /** * Convert Base64 to data * @param data */ base64Decode(data: string): any; /** * MD5 hash * @param data */ md5(data: string): string; /** * Save file * @param filename * @param data * @param option */ save( filename: string, data: string | Uint8Array, option?: { isBase64?: boolean; } ): boolean; }; } declare var focusany: FocusAnyApi; ================================================ FILE: sdk/index.d.ts ================================================ /// // 默认导出 focusany API declare const focusany: FocusAnyApi; export = focusany; ================================================ FILE: sdk/index.ts ================================================ /// export = focusany ================================================ FILE: sdk/package.json ================================================ { "name": "focusany-sdk", "version": "1.3.8", "description": "TypeScript definitions for FocusAny", "main": "./index.js", "types": "./index.d.ts", "bin": { "focusany": "./bin/command.js" }, "repository": { "type": "git", "url": "https://github.com/modstart-lib/focusany" }, "keywords": [ "FocusAny" ], "author": "ModStart", "license": "Apache-2.0", "bugs": { "url": "https://github.com/modstart-lib/focusany/issues" }, "homepage": "https://github.com/modstart-lib/focusany#readme", "scripts": { "build": "npx tsc --project tsconfig.json", "test": "node tests/test-release-prepare.js" }, "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" }, "./shim": { "types": "./focusany-shim.d.ts", "default": "./focusany-shim.js" } }, "devDependencies": { "@babel/cli": "^7.28.0", "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@types/node": "^20.0.0", "terser": "^5.43.1", "typescript": "^5.0.0" } } ================================================ FILE: sdk/shim.html ================================================ FocusAny Shim Test

🚀 FocusAny Shim Version Test

Status: Initializing...

📋 Execution Log:

Waiting for test...
================================================ FILE: sdk/tests/config.json ================================================ { "development": { "env": "dev", "debug": true, "apiEndpoint": "https://api-dev.example.com" }, "production": { "env": "prod", "debug": false, "apiEndpoint": "https://api.example.com" }, "test": { "enabled": true, "mockResponses": true, "timeout": 5000 } } ================================================ FILE: sdk/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "module": "CommonJS", "lib": [ "ES2020", "DOM" ], "outDir": "./", "rootDir": "./", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": false, "moduleResolution": "node" }, "include": [ "*.ts", "*/**.ts" ], "exclude": [ "node_modules", "*.js", "*/**.js", "*.d.ts" ] } ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/api/types/base.ts ================================================ interface ApiResult { code: number; msg: string; data: T; } ================================================ FILE: src/api/user.ts ================================================ import { request } from "../lib/api"; export function userInfoApi(): Promise< ApiResult<{ apiToken: string; user: object; data: any; basic: object; }> > { return request({ url: "app_manager/user_info", method: "post", }); } ================================================ FILE: src/app/dragWindow.ts ================================================ export const useDragWindow = ({ name, ignore, }: { name: string | null; ignore?: (e: MouseEvent) => boolean; }) => { name = name || null; let animationId: number; let mouseX: number; let mouseY: number; let clientWidth = 0; let clientHeight = 0; let draggable = true; const onDragWindowMouseDown = (e) => { // 右击不移动 if (e.button === 2) { return; } if (ignore && ignore(e)) { return; } draggable = true; mouseX = e.clientX; mouseY = e.clientY; if (Math.abs(document.body.clientWidth - clientWidth) > 5) { clientWidth = document.body.clientWidth; } if (Math.abs(document.body.clientHeight - clientHeight) > 5) { clientHeight = document.body.clientHeight; } document.addEventListener("mouseup", onMouseUp); animationId = requestAnimationFrame(moveWindow); }; const onMouseUp = () => { draggable = false; document.removeEventListener("mouseup", onMouseUp); cancelAnimationFrame(animationId); }; const moveWindow = () => { window.$mapi.app .windowMove(name, { mouseX, mouseY, width: clientWidth, height: clientHeight, }) .then(() => { if (draggable) { animationId = requestAnimationFrame(moveWindow); } }); }; return { onDragWindowMouseDown, }; }; ================================================ FILE: src/app/locale.ts ================================================ import zhCN from "@arco-design/web-vue/es/locale/lang/zh-cn"; import enUS from "@arco-design/web-vue/es/locale/lang/en-us"; import { onLocaleChange } from "../lang"; import { ref } from "vue"; export const useLocale = () => { const locales = { "zh-CN": zhCN, "en-US": enUS, }; const locale = ref(zhCN); onLocaleChange((newLocale) => { locale.value = locales[newLocale]; }); return { locale: locale, }; }; ================================================ FILE: src/components/AppQuitConfirm.vue ================================================ ================================================ FILE: src/components/PageNav.vue ================================================ ================================================ FILE: src/components/Setting/SettingAbout.vue ================================================ ================================================ FILE: src/components/Setting/SettingBasic.vue ================================================ ================================================ FILE: src/components/Setting/SettingEnv.vue ================================================ ================================================ FILE: src/components/Setting/components/SettingEnvHubRoot.vue ================================================ ================================================ FILE: src/components/TextTruncateView.vue ================================================ ================================================ FILE: src/components/common/AudioPlayer.vue ================================================ ================================================ FILE: src/components/common/CodeViewer.vue ================================================ ================================================ FILE: src/components/common/CodeViewerDialog.vue ================================================ ================================================ FILE: src/components/common/DataConfigDialogButton.vue ================================================ ================================================ FILE: src/components/common/DragPasteContainer.vue ================================================ ================================================ FILE: src/components/common/FeedbackTicketButton.vue ================================================ ================================================ FILE: src/components/common/FileExt.vue ================================================ ================================================ FILE: src/components/common/FileLogViewer.vue ================================================ ================================================ FILE: src/components/common/FilesSelector.vue ================================================ ================================================ FILE: src/components/common/HtmlViewer.vue ================================================ ================================================ FILE: src/components/common/InputInlineEditor.vue ================================================ ================================================ FILE: src/components/common/LogViewer.vue ================================================ ================================================ FILE: src/components/common/LogViewerDialog.vue ================================================ ================================================ FILE: src/components/common/MEmpty.vue ================================================ ================================================ FILE: src/components/common/MLoading.vue ================================================ ================================================ FILE: src/components/common/PageWebviewStatus.vue ================================================ ================================================ FILE: src/components/common/ProUpgrade.vue ================================================ ================================================ FILE: src/components/common/SettingItemYesNo.vue ================================================ ================================================ FILE: src/components/common/SettingItemYesNoDefault.vue ================================================ ================================================ FILE: src/components/common/TaskBizStatus.vue ================================================ ================================================ FILE: src/components/common/UpdaterButton.vue ================================================ ================================================ FILE: src/components/common/VideoPlayer.vue ================================================ ================================================ FILE: src/components/common/WebFileSelectButton.vue ================================================ ================================================ FILE: src/components/common/dataConfig.ts ================================================ export const getDataContent = async ( key: string, defaultValue: T, ): Promise => { return $mapi.storage.get("data", key, defaultValue); }; ================================================ FILE: src/components/common/index.ts ================================================ ================================================ FILE: src/components/common/util.ts ================================================ import { onMounted, toRaw, watch } from "vue"; import { AppConfig } from "../../config"; import { t } from "../../lang"; import { defaultResponseProcessor } from "../../lib/api"; import { Dialog } from "../../lib/dialog"; import { StorageUtil } from "../../lib/storage"; import { VersionUtil } from "../../lib/util"; export const doCopy = async ( text: string | object, successTip: string = "", ): Promise => { successTip = successTip || t("common.copySuccess"); text = typeof text === "object" ? JSON.stringify(text) : String(text); await window.$mapi.app.setClipboardText(text); Dialog.tipSuccess(successTip); }; export const doSaveFile = async (filePath: string) => { try { const options: any = { defaultPath: window.$mapi.file.pathToName(filePath, true, -1), }; const savePath = await window.$mapi.file.openSave(options); if (savePath) { await window.$mapi.file.copy(filePath, savePath, { isDataPath: false, }); Dialog.tipSuccess(t("msg.fileSavedTo", { path: savePath })); } } catch (error) { Dialog.tipError( t("error.saveFileFailed", { error: (error as Error).message || error, }), ); } }; export const doOpenFile = async (options?: { extensions?: string[]; multiple?: boolean; }): Promise => { options = Object.assign( { extensions: [], multiple: false, }, options, ); try { const opt: any = {}; if (options.extensions && options.extensions.length > 0) { opt.filters = [ { extensions: toRaw(options.extensions), }, ]; } if (options.multiple) { opt.properties = ["multiSelections"]; } const result = await window.$mapi.file.openFile(opt); if (result) { return result; } } catch (error) { Dialog.tipError(t("error.selectFileFailed", { error })); } }; export const doOpenBrowserFile = (options: { accept: string; multiple: boolean; max?: string; }): Promise => { options = Object.assign({ accept: "*/*", multiple: false, max: undefined, }); const compareSize = (size: number, target: string): boolean => { const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = sizes.findIndex((item) => item === target.replace(/\d+/, "")); return size > parseInt(target) * k ** i; }; return new Promise((resolve, reject) => { // 创建input[file]元素 const input = document.createElement("input"); // 设置相应属性 input.setAttribute("type", "file"); input.setAttribute("accept", options.accept); if (options.multiple) { input.setAttribute("multiple", "multiple"); } else { input.removeAttribute("multiple"); } // 绑定事件 input.onchange = function () { // @ts-ignore let files: File[] = Array.from(this.files); if (files) { const length = files.length; files = files.filter((file) => { if (options.max) { return !compareSize(file.size, options.max); } else { return true; } }); if (files && files.length > 0) { if (length !== files.length) { // message.warning(`已过滤上传文件中大小大于${options.max}的文件`); } resolve(files[0]); } else { Dialog.tipError( t("error.fileSizeExceedMax", { max: options.max }), ); resolve(null); } } else { reject(null); } }; input.oncancel = function () { reject(null); }; input.click(); }); }; export const doCheckForUpdate = async (noticeLatest?: boolean) => { const res = await window.$mapi.updater.checkForUpdate(); defaultResponseProcessor(res, (res: ApiResult) => { if (!res.data.version) { Dialog.tipError(t("update.checkFailed")); return; } if (VersionUtil.le(res.data.version, AppConfig.version)) { if (noticeLatest) { Dialog.tipSuccess(t("update.alreadyLatest")); } return; } Dialog.confirm( t("update.newVersionFound", { version: res.data.version }), ).then(() => { window.$mapi.app.openExternal(AppConfig.downloadUrl); }); }); }; export const dataAutoSaveDraft = ( key: string, data: any, option?: { type: "object" | "array"; confirmText?: string | null; }, ) => { option = Object.assign( { type: "object", confirmText: null, }, option, ); const load = async () => { const value = await result(); if ("object" === option?.type) { if (value) { if (option.confirmText) { await Dialog.confirm(option.confirmText); } for (const k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { data[k] = value[k]; } } } } else if ("array" === option?.type) { if (Array.isArray(value) && value.length > 0) { if (option.confirmText) { await Dialog.confirm(option.confirmText); } data.splice(0, data.length, ...value); } } }; onMounted(async () => [await load()]); watch( () => data, async (value) => { // console.log('data changed, save draft to local storage', key, value); StorageUtil.set(key, value); }, { deep: true, }, ); const clearDraft = () => { StorageUtil.remove(key); }; const result = async () => { if ("object" === option?.type) { return StorageUtil.getObject(key); } else if ("array" === option?.type) { return StorageUtil.getArray(key); } throw new Error("dataAutoSaveDraft: unknown type" + option?.type); }; return { clearDraft, load, }; }; ================================================ FILE: src/config.ts ================================================ import packageJson from "../package.json"; const BASE_URL = "https://focusany.com"; // const BASE_URL = 'http://focusany.demo.soft.host'; export const AppConfig = { name: "FocusAny", title: "FocusAny", slogan: "专注提效的AI工具条", version: packageJson.version, website: `${BASE_URL}`, websiteGithub: "https://github.com/modstart-lib/focusany", websiteGitee: "https://gitee.com/modstart-lib/focusany", apiBaseUrl: `${BASE_URL}/api`, updaterUrl: `${BASE_URL}/app_manager/updater/open`, downloadUrl: `${BASE_URL}/app_manager/download`, feedbackUrl: `${BASE_URL}/feedback_ticket`, statisticsUrl: `${BASE_URL}/app_manager/collect`, guideUrl: `${BASE_URL}/app_manager/guide`, helpUrl: `${BASE_URL}/app_manager/help`, basic: { userEnable: false, }, }; ================================================ FILE: src/declarations/svg.d.ts ================================================ declare module "*.svg" { const content: string; export default content; } ================================================ FILE: src/declarations/type.d.ts ================================================ type DefsPage = { onShow: (cb: Function) => void; onHide: (cb: Function) => void; onMaximize: (cb: Function) => void; onUnmaximize: (cb: Function) => void; onEnterFullScreen: (cb: Function) => void; onLeaveFullScreen: (cb: Function) => void; onBroadcast: (type: string, cb: (data: any) => void) => void; offBroadcast: (type: string, cb: (data: any) => void) => void; registerCallPage: ( name: string, cb: ( resolve: (data: any) => void, reject: (error: string) => void, data: any, ) => void, ) => void; createChannel: (cb: (data: any) => void) => string; destroyChannel: (channel: string) => void; ipcSendToHost: (channel: string, type: string, data?: any) => void; ipcSend: (channel: string, type: string, data?: any) => void; onPluginInit: (cb: Function) => void; onPluginInitReady: (cb: Function) => void; onPluginAlreadyOpened: (cb: Function) => void; onPluginExit: (cb: Function) => void; onPluginDetached: (cb: Function) => void; onPluginState: (cb: Function) => void; onPluginCodeInit: (cb: Function) => void; onPluginCodeSetting: (cb: Function) => void; onPluginCodeData: (cb: Function) => void; onPluginCodeExit: (cb: Function) => void; onSetSubInput: (cb: Function) => void; onRemoveSubInput: (cb: Function) => void; onSetSubInputValue: (cb: Function) => void; onDetachSet: (cb: Function) => void; onDetachWindowClosed: (cb: Function) => void; }; type DefsMapi = { app: { getPreload: () => Promise; resourcePathResolve: (filePath: string) => Promise; extraPathResolve: (filePath: string) => Promise; platformName: () => "win" | "osx" | "linux" | null; platformArch: () => "x86" | "arm64" | null; isPlatform: (platform: "win" | "osx" | "linux") => boolean; quit: () => Promise; restart: () => Promise; windowMin: (name?: string) => Promise; windowMax: (name?: string) => Promise; windowSetSize: ( name: string | null, width: number, height: number, option?: { includeMinimumSize?: boolean; center?: boolean; }, ) => Promise; windowOpen: (name: string, option?: any) => Promise; windowHide: (name?: string) => Promise; windowClose: (name?: string) => Promise; windowMove: ( name: string | null, data: { mouseX: number; mouseY: number; width: number; height: number; }, ) => Promise; openExternal: (url: string) => Promise; openPath: (url: string) => Promise; showItemInFolder: (url: string) => Promise; appEnv: () => Promise; setRenderAppEnv: (env: any) => Promise; isDarkMode: () => Promise; shell: ( command: string, option?: { cwd?: string; outputEncoding?: string; shell?: boolean; }, ) => Promise; spawnShell: ( command: string | string[], option: { stdout?: (data: string, process: any) => void; stderr?: (data: string, process: any) => void; success?: (process: any) => void; error?: (msg: string, exitCode: number, process: any) => void; cwd?: string; outputEncoding?: string; env?: Record; shell?: boolean; } | null, ) => Promise<{ stop: () => void; send: (data: any) => void; result: () => Promise; }>; spawnBinary: ( binary: string, args: string[], option?: { stdout?: (data: string, process: any) => void; stderr?: (data: string, process: any) => void; success?: (process: any) => void; error?: (msg: string, exitCode: number, process: any) => void; cwd?: string; outputEncoding?: string; env?: Record; shell?: boolean; } | null, ) => Promise; availablePort: ( start: number, lockKey?: string, lockTime?: number, ) => Promise; fixExecutable: (executable: string) => Promise; getClipboardText: () => Promise; setClipboardText: (text: string) => Promise; getClipboardImage: () => Promise; setClipboardImage: (image: string) => Promise; getUserAgent: () => string; toast: ( msg: string, option?: { duration?: number; status?: "success" | "error"; }, ) => Promise; setupList: () => Promise< { name: string; title: string; status: "success" | "fail"; desc: string; steps: { title: string; image: string; }[]; }[] >; setupOpen: (name: string) => Promise; setupIsOk: () => Promise; getBuildInfo: () => Promise<{ buildTime: string; }>; collect: (options?: {}) => Promise; setAutoLaunch: (enable: boolean, options?: {}) => Promise; getAutoLaunch: (options?: {}) => Promise; }; config: { get: (key: string, defaultValue: any = null) => Promise; set: (key: string, value: any) => Promise; all: () => Promise; getEnv: (key: string, defaultValue: any = null) => Promise; setEnv: (key: string, value: any) => Promise; allEnv: () => Promise; }; log: { root: () => string; info: (msg: string, data: any = null) => Promise; error: (msg: string, data: any = null) => Promise; collect: (option?: { startTime?: string; endTime?: string; limit?: number; }) => Promise; }; storage: { all: () => Promise; get: (group: string, key: string, defaultValue: any) => Promise; set: (group: string, key: string, value: any) => Promise; write: (group: string, value: any) => Promise; read: (group: string, defaultValue: any = null) => Promise; }; db: { execute: (sql: string, params: any = []) => Promise; insert: (sql: string, params: any = []) => Promise; first: (sql: string, params: any = []) => Promise; select: (sql: string, params: any = []) => Promise; update: (sql: string, params: any = []) => Promise; delete: (sql: string, params: any = []) => Promise; }; kvdb: { put: (name: string, data: Doc) => Promise; putForce: (name: string, data: Doc) => Promise; get: (name: string, id: string) => Promise; remove: (name: string, doc: Doc | string) => Promise; bulkDocs: (name: string, docs: any[]) => Promise; allDocs: (name: string, key: string) => Promise; allKeys: (name: string, key: string) => Promise; count: (name: string, key: string) => Promise; postAttachment: ( name: string, docId: string, attachment: any, type: string, ) => Promise; getAttachment: (name: string, docId: string) => Promise; getAttachmentType: (name: string, docId: string) => Promise; dumpToFile: (file: string) => Promise; importFromFile: (file: string) => Promise; testWebdav: (option: { url: string; username: string; password: string; }) => Promise; dumpToWebDav: ( file: string, option: { url: string; username: string; password: string; }, ) => Promise; importFromWebDav: ( file: string, option: { url: string; username: string; password: string; }, ) => Promise; listWebDav: ( dir: string, option: { url: string; username: string; password: string; }, ) => Promise; }; file: { fullPath: (path: string) => Promise; exists: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; isDirectory: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; mkdir: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; list: ( path: string, option?: { isDataPath?: boolean }, ) => Promise< { name: string; pathname: string; isDirectory: boolean; size: number; lastModified: number; }[] >; listAll: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; write: ( path: string, data: any, option?: { isDataPath?: boolean }, ) => Promise; writeBuffer: ( path: string, data: any, option?: { isDataPath?: boolean }, ) => Promise; writeStream: ( path: string, stream: any, option?: { isDataPath?: boolean }, ) => Promise; read: (path: string, option?: { isDataPath?: boolean }) => Promise; readBuffer: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; readStream: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; deletes: ( path: string, option?: { isDataPath?: boolean }, ) => Promise; clean: ( paths: string[], option?: { isDataPath?: boolean }, ) => Promise; rename: ( pathOld: string, pathNew: string, option?: { isDataPath?: boolean; overwrite?: boolean; }, ) => Promise; copy: ( pathOld: string, pathNew: string, option?: { isDataPath?: boolean; overwrite?: boolean; }, ) => Promise; temp: ( ext: string = "tmp", prefix: string = "file", suffix: string = "", ) => Promise; tempDir: (prefix: string = "dir") => Promise; watchText: ( path: string, callback: (data: {}) => void, option?: { isDataPath?: boolean; limit?: number; }, ) => Promise<{ stop: Function; }>; appendText: ( path: string, data: any, option?: { isDataPath?: boolean }, ) => Promise; download: ( url: string, path?: string | null, option?: { isDataPath?: boolean; userAgent?: string; progress?: (percent: number, total: number) => void; }, ) => Promise; openFile: ( options: { filters?: { name: string; extensions: string[]; }[]; properties?: "multiSelections"[]; } = {}, ) => Promise; openDirectory: (options: {} = {}) => Promise; openSave: (options: {} = {}) => Promise; ext: (path: string) => Promise; stat: ( path: string, option?: { isDataPath?: boolean }, ) => Promise<{ size: number; isDirectory: boolean; lastModified: number; }>; textToName: ( text: string, ext: string = "", maxLimit: number = 100, ) => string; pathToName: ( path: string, includeExt: boolean = true, maxLimit: number = 100, ) => string; hubRootDefault: () => Promise; hubRoot: () => Promise; hubSave: ( file: string, option?: { ext?: string; returnFullPath?: boolean; ignoreWhenInHub?: boolean; cleanOld?: boolean; saveGroup?: string; savePath?: string; savePathParam?: { [key: string]: any; }; }, ) => Promise; hubSaveContent: ( content: string, option: { ext: string; returnFullPath?: boolean; saveGroup?: string; savePath?: string; savePathParam?: { [key: string]: any; }; }, ) => Promise; hubDelete: ( file: string, option?: { isDataPath?: boolean; ignoreWhenNotInHub?: boolean; tryLaterWhenFailed?: boolean; }, ) => Promise; hubFullPath: (file: string) => Promise; hubFile: ( ext: string, option?: { returnFullPath?: boolean; saveGroup?: string; savePath?: string; savePathParam?: { [key: string]: any; }; }, ) => Promise; isHubFile: (file: string) => Promise; cacheForget: (key: any) => Promise; cacheSet: (key: any, data: any) => Promise; cacheGet: (key: any) => Promise; cacheGetPath: (key: any) => Promise; }; updater: { checkForUpdate: () => Promise>; getCheckAtLaunch: () => Promise<"yes" | "no">; setCheckAtLaunch: (value: "yes" | "no") => Promise; }; statistics: { tick: (name: string, data: any = null) => Promise; }; event: { send: (name: string, type: string, data: any) => void; callPage: ( name: string, type: string, data?: any, option?: { waitReadyTimeout?: number; timeout?: number; }, ) => Promise>; channelSend: (channel: string, data: any) => Promise; }; user: { open: (option?: { readyParam: { page?: string; [key: string]: any; }; }) => Promise; get: () => Promise<{ apiToken: string; user: { id: string; name: string; avatar: string; }; data: {}; basic: {}; }>; refresh: () => Promise; getApiToken: () => Promise; getWebEnterUrl: (url: string) => Promise; openWebUrl: (url: string) => Promise; apiPost: ( url: string, data: Record, option?: { throwException?: boolean; }, ) => Promise; }; misc: { getZipFileContent: (path: string, pathInZip: string) => Promise; unzip: ( zipPath: string, dest: string, option?: { process: Function }, ) => Promise; zip: ( zipPath: string, sourceDir: string, option?: { end?: (archive: any) => void }, ) => Promise; request: (option: { url: string; method?: "GET" | "POST"; responseType?: "json" | "text" | "arraybuffer"; headers?: any; data?: any; }) => Promise; }; manager: { getConfig: () => Promise; setConfig: (config: ConfigRecord) => Promise; getMcpServer: () => Promise; getMcpInfo: () => Promise<{ tools: { name: string; description: string }[]; }>; isShown: () => Promise; show: () => Promise; hide: () => Promise; getClipboardContent: () => Promise; getClipboardChangeTime: () => Promise; getSelectedContent: () => Promise; searchFastPanelAction: ( query: { currentFiles: any[]; currentImage: string; }, option?: {}, ) => Promise<{ matchActions: ActionRecord[]; viewActions: ActionRecord[]; }>; listDetachWindowActions: (option?: {}) => Promise; searchAction: ( query: { keywords: string; currentFiles: any[]; currentImage: string; }, option?: {}, ) => Promise<{ detachWindowActions: ActionRecord[]; searchActions: ActionRecord[]; matchActions: ActionRecord[]; viewActions: ActionRecord[]; historyActions: ActionRecord[]; pinActions: ActionRecord[]; }>; subInputChange: (keywords: string, option?: {}) => void; openPlugin: (pluginName: string, option?: {}) => Promise; openAction: (action: ActionRecord) => Promise; openActionCode: (id: string | null) => Promise; searchActionCode: (keywords: string | null) => Promise; openActionWindow: (type: "open", action: ActionRecord) => Promise; closeMainPlugin: (option?: {}) => Promise; openMainPluginDevTools: (option?: {}) => Promise; openMainPluginLog: (option?: {}) => Promise; detachPlugin: (option?: {}) => Promise; listPlugin: (option?: {}) => Promise; installPlugin: (fileOrPath: string, option?: {}) => Promise; refreshInstallPlugin: ( pluginName: string, option?: {}, ) => Promise; uninstallPlugin: (pluginName: string, option?: {}) => Promise; getPluginInstalledVersion: ( pluginName: string, option?: {}, ) => Promise; listDisabledActionMatch: (option?: {}) => Promise; toggleDisabledActionMatch: ( pluginName: string, actionName: string, matchName: string, option?: {}, ) => Promise; listPinAction: (option?: {}) => Promise; togglePinAction: ( pluginName: string, actionName: string, option?: {}, ) => Promise; showLog: (pluginName: string, option?: {}) => Promise; clearCache: (option?: {}) => Promise; hotKeyWatch: (option?: {}) => Promise; hotKeyUnwatch: (option?: {}) => Promise; toggleDetachPluginAlwaysOnTop: ( alwaysOnTop: boolean, option?: {}, ) => Promise; setDetachPluginZoom: (zoom: number, option?: {}) => Promise; firePluginMoreMenuClick: (name: string, option?: {}) => Promise; fireDetachOperateClick: (name: string, option?: {}) => Promise; closeDetachPlugin: (option?: {}) => Promise; openDetachPluginDevTools: (option?: {}) => Promise; openDetachPluginLog: (option?: {}) => Promise; setPluginAutoDetach: ( autoDetach: boolean, option?: {}, ) => Promise; getPluginConfig: ( pluginName: string, option?: {}, ) => Promise; listFilePluginRecords: (option?: {}) => Promise; updateFilePluginRecords: ( records: FilePluginRecord[], option?: {}, ) => Promise; listLaunchRecords: (option?: {}) => Promise; updateLaunchRecords: ( records: LaunchRecord[], option?: {}, ) => Promise; storeInstall: ( pluginName: string, option?: { version?: string; }, ) => Promise; storePublish: ( pluginName: string, option?: { version?: string; }, ) => Promise; storePublishInfo: ( pluginName: string, option?: { version?: string; }, ) => Promise; storeInstallingInfo: (pluginName: string) => Promise<{ isInstalling: boolean; percent: number; }>; historyClear: (option?: {}) => Promise; historyDelete: ( pluginName: string, actionName: string, option?: {}, ) => Promise; }; }; declare global { interface Window { __page: DefsPage; $mapi: DefsMapi; focusany: FocusAnyApi; } const __page: DefsPage; const $mapi: DefsMapi; const focusany: FocusAnyApi; } export {}; ================================================ FILE: src/entry/Page.vue ================================================ ================================================ FILE: src/entry/about.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageAbout from "../pages/PageAbout.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "about", title: t("about.title"), page: PageAbout, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/detachWindow.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageDetachWindow from "../pages/PageDetachWindow.vue"; const app = createApp(PageDetachWindow); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/fastPanel.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageFastPanel from "../pages/PageFastPanel.vue"; const app = createApp(PageFastPanel); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/feedback.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageFeedback from "../pages/PageFeedback.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "feedback", title: t("nav.feedback"), page: PageFeedback, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/guide.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageGuide from "../pages/PageGuide.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "guide", title: t("nav.guide"), page: PageGuide, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/log.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageLog from "../pages/PageLog.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "log", title: t("nav.log"), page: PageLog, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/monitor.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageMonitor from "../pages/PageMonitor.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "monitor", title: t("common.loading"), page: PageMonitor, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/payment.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PagePayment from "../pages/PagePayment.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "payment", title: t("page.payment.title"), page: PagePayment, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/setup.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageSetup from "../pages/PageSetup.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "setup", title: t("page.setup.title"), page: PageSetup, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/store.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageStore from "../pages/PageStore.vue"; const app = createApp(PageStore); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/system.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageSystem from "../pages/PageSystem.vue"; const app = createApp(PageSystem); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/user.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageUser from "../pages/PageUser.vue"; import Page from "./Page.vue"; const app = createApp(Page, { name: "user", title: t("nav.userCenter"), page: PageUser, }); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/entry/workflow.ts ================================================ import { createApp } from "vue"; import store from "../store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import { i18n, t } from "../lang"; import { Dialog } from "../lib/dialog"; import "../style.less"; import PageWorkflow from "../pages/PageWorkflow.vue"; const app = createApp(PageWorkflow); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); }); ================================================ FILE: src/hooks/user.ts ================================================ import { ref } from "vue"; import { useUserStore } from "../store/modules/user"; import { useSettingStore } from "../store/modules/setting"; const setting = useSettingStore(); export const useUserPage = ({ web, status }) => { const webPreload = ref(""); const webUrl = ref(""); const webUserAgent = window.$mapi.app.getUserAgent(); const user = useUserStore(); const canGoBack = ref(false); const whiteUrl = [ "/app_manager/user", "/member_vip", "/login", "/register", "/logout", ]; const urlMap = { "/app_manager/user": "/member", }; const getUrl = () => { const url = web.value.getURL(); return new URL(url).pathname; }; const getCanGoBack = () => { if (whiteUrl[0] === getUrl()) { return false; } return true; }; const doBack = async () => { web.value.loadURL(await user.webUrl()); }; const onMount = async () => { web.value.addEventListener("did-fail-load", (event: any) => { status.value?.setStatus("fail"); }); web.value.addEventListener("did-finish-load", (event: any) => { if (setting.shouldDarkMode()) { web.value.executeJavaScript( `document.body.setAttribute('data-theme', 'dark');`, ); } }); web.value.addEventListener("close", (event: any) => { if (web.value.isDevToolsOpened()) { web.value.closeDevTools(); } }); web.value.addEventListener("dom-ready", (e) => { // web.value.openDevTools(); window.$mapi.user.refresh(); canGoBack.value = getCanGoBack(); web.value.executeJavaScript(` document.addEventListener('click', (event) => { const target = event.target; if (target.tagName !== 'A') return; let url = target.href if(url.startsWith('javascript:')) return; let urlPath = new URL(url).pathname; const urlMap = ${JSON.stringify(urlMap)}; if(urlMap[urlPath]) { urlPath = urlMap[urlPath]; const urlNew = new URL(url); urlNew.pathname = urlPath; url = urlNew.toString(); } const whiteList = ${JSON.stringify(whiteUrl)}; if (whiteList.includes(urlPath)) return; event.preventDefault(); window.$mapi.user.openWebUrl(url) }); `); status.value?.setStatus("success"); if (window.__page) { window.__page.registerCallPage( "ready", (resolve, reject, data) => { web.value.executeJavaScript( `var call = function(){ if(!window.__appManagerUserReady){ setTimeout(call,10); return; }; window.__appManagerUserReady(${JSON.stringify(data)}); };call();`, ); resolve(undefined); }, ); } }); status.value?.setStatus("loading"); webPreload.value = await window.$mapi.app.getPreload(); webUrl.value = await user.webUrl(); }; return { webPreload, webUrl, webUserAgent, user, canGoBack, doBack, onMount, }; }; ================================================ FILE: src/lang/en-US.json ================================================ { "config.slogan": "Focused AI Productivity Bar", "about.disclaimer": "Disclaimer", "about.license": "This product is open source software, following the AGPL-3.0 license agreement.", "about.software": "About Software", "about.title": "About", "action.attrMatch": "Attribute Match", "action.builtin": "Builtin", "action.fileExtensions": "File Extensions", "action.fileType": "File Type", "action.matchAction": "Match Action", "action.matchEditorHint": "When file matches the following extensions and types", "action.matchExactKeyHint": "Input exactly equals the following keyword", "action.matchFileHint": "When matched using the following rules", "action.matchImageHint": "When an image is matched", "action.matchKeywordHint": "Input matches the following keywords, including full pinyin and initials", "action.matchRegexHint": "Input matches the following regex", "action.matchWindowHint": "When the active window matches the following conditions", "action.maxCount": "Max Count", "action.minCount": "Min Count", "action.nameMatch": "Name Match", "action.pinToSearch": "Pin to Search Bar", "action.plugin": "Plugin", "action.regex": "Regex", "action.searchAction": "Search Action", "action.suffix": "Suffix", "action.titleMatch": "Title Match", "action.unpinFromSearch": "Unpin from Search Bar", "action.window": "Window", "avatar.addVideo": "Add Video Avatar", "avatar.audioToVideo": "Audio Driven Lip Sync Video", "avatar.avatar": "Digital Human Avatar", "avatar.canOpenCloseMouth": "Mouth Can Open/Close", "avatar.config": "Digital Human Configuration", "avatar.digitalHuman": "Digital Human", "avatar.example": "Avatar Example", "avatar.faceInterference": "Face Interference", "avatar.live": "Digital Human Live Stream", "avatar.manage": "Manage Multiple Avatars", "avatar.model": "Digital Human Model", "avatar.oneClickSynthesis": "One-Click Synthesis", "avatar.saveToMine": "Save to My Avatars", "avatar.selfie": "Frontal Selfie", "avatar.smartLive": "Smart Live Stream", "avatar.synthesis": "Digital Human Synthesis", "avatar.video": "Video Avatar", "avatar.videoReq": "Video Avatar Requirements", "backup.backingUp": "Backing up...", "backup.backupFailed": "Backup Failed", "backup.backupSuccess": "Backup Success", "backup.backupToFile": "Backup to File", "backup.backupToLocal": "Backup to Local", "backup.formatTip": "Backup uses the backup format, regularly backing up files can avoid data loss.", "backup.restoreFromFile": "Restore from File", "backup.restoreFromLocal": "Restore from Local", "backup.restoreFailed": "Restore Failed", "backup.restoreSuccess": "Restore Success", "backup.restoring": "Restoring...", "backup.title": "Backup/Restore", "common.adapt": "Adapt", "common.add": "Add", "common.addFile": "Add File", "common.addOne": "Add One", "common.aiGenerated": "AI Generated", "common.all": "All", "common.and": "And", "common.availableVars": "Available Variables", "common.back": "Back", "common.batchInput": "Batch Input", "common.batchPaste": "Batch Paste", "common.batchPasteHint": "Batch paste, one per line", "common.cancel": "Cancel", "common.check": "Check", "common.clearConfirm": "Confirm Clear?", "common.clearHistory": "Clear History", "common.clickTextToCopy": "Click text to copy", "common.clickToConfig": "Click to Configure", "common.clickToCopy": "Click to Copy", "common.close": "Close", "common.collapse": "Collapse", "common.confirm": "Confirm", "common.copySuccess": "Copy Success", "common.copyText": "Copy Text", "common.default": "Default", "common.disable": "Disable", "common.enable": "Enable", "common.open": "Open", "common.delete": "Delete", "common.deleteConfirm": "Confirm Delete?", "common.deleteRecordsConfirm": "Delete {count} records?", "common.description": "Description", "common.detail": "Details", "common.docs": "Docs", "common.download": "Download", "common.downloadFailed": "Download Failed", "common.downloadSuccess": "Download Successful", "common.downloadingWait": "Downloading, please wait...", "common.duration": "Duration", "common.edit": "Edit", "common.error": "Error", "common.exit": "Exit", "common.exitConfirm": "Confirm Exit?", "common.expand": "Expand", "common.extensions": "{extensions}", "common.failed": "Failed", "common.feature": "Feature", "common.file": "File", "common.find": "Find", "common.folder": "Folder", "common.generate": "Generate", "common.generateFailed": "Generation Failed", "common.hideWindow": "Hide Window", "common.image": "Image", "common.info": "Info", "common.inputContent": "Input Content", "common.key": "Key", "common.keywords": "Keywords", "common.language": "Language", "common.loading": "Loading", "common.loadingDots": "Loading...", "common.localFile": "Local File", "common.loginRequired": "Login Required", "common.merge": "Merge", "common.mergeWhitespace": "Merge Whitespace", "common.modify": "Modify", "common.more": " More", "common.moreDetails": "More Details", "common.name": "Name", "common.no": "No", "common.none": "None", "common.notLoggedIn": "Not Logged In", "common.officialSite": "Official Site", "common.onlineDocs": "Online Docs", "common.openFile": "Open File", "common.openPath": "Open Path", "common.pause": "Pause", "common.pro": "Pro", "common.recharge": "Recharge", "common.refresh": "Refresh", "common.rememberChoice": "Remember my choice", "common.replace": "Replace", "common.reselect": "Reselect", "common.resolution": "Resolution", "common.restoreDefault": "Restore Default", "common.resumeSuccess": "Resume Success", "common.retryAttempt": "Retry Attempt", "common.retryFailed": "Retry Failed", "common.retrySuccess": "Retry Success", "common.save": "Save", "common.saveSuccess": "Save Success", "common.select": "Select", "common.selectAll": "Select All", "common.selectFile": "Select File", "common.selectLocalFile": "Select Local File", "common.selectPath": "Select Path", "common.sendFailed": "Send Failed", "common.sendSuccess": "Send Success", "common.service": "Service", "common.setting": "Settings", "common.settingSuccess": "Setting Success", "common.split": "Split", "common.stopFailed": "Stop Failed", "common.stopService": "Stop Service", "common.stopping": "Stopping", "common.submitConfirm": "Confirm Submit", "common.success": "Success", "common.system": "System", "common.tag": "Tag", "common.test": "Test", "common.testFailed": "Test Failed", "common.testSuccess": "Test Success", "common.testing": "Testing, please wait...", "common.tip": "Tip", "common.totalCount": "Total {count} items", "common.totalWords": "Total {count} words", "common.type": "Type", "common.useNow": "Use Now", "common.value": "Value", "common.version": "Version", "common.view": "View", "common.viewCode": "View Code", "common.viewEffect": "View Effect", "common.viewRecord": "View Record", "common.vipRequired": "VIP Required", "common.yes": "Yes", "dashboard.statistics": "Statistics", "dashboard.today": "Today", "dashboard.todayTotalTasks": "Today's Total Tasks", "desc.img2img": "Generate new image based on input image + description prompt", "desc.longTextToAudio": "Convert long text content to audio file", "desc.recognitionDownload": "Recognize file and download text/subtitle", "desc.recognitionEdit": "Recognize audio file, support editing/downloading text/subtitle file after recognition", "desc.subtitleToAudio": "Convert subtitle file to audio file", "desc.txt2img": "Generate image based on text description", "desc.videoVoiceReplace": "Replace human voice in video with other timbre", "download.audio": "Download Audio", "download.subtitleFile": "Download Subtitle File", "download.textFile": "Download Text File", "empty.noDownloadRecord": "No download records available", "empty.noEditableData": "No editable data", "empty.noLocalModel": "No local model available", "empty.noLog": "No logs", "empty.noLogFile": "No log file", "empty.noModel": "No model available", "empty.noModelAdd": "No models yet, please add a model~", "empty.noModelPlatform": "No relevant model platform found", "empty.noRecognitionTask": "No voice recognition tasks", "empty.noRecord": "No records available", "empty.noVoiceTask": "No voice synthesis tasks", "error.pluginAlreadyExists": "Plugin Already Exists", "error.pluginEditionNotMatch": "FocusAny Edition Does Not Meet Plugin Requirements", "error.pluginFormatError": "Plugin Format Error", "error.pluginNotExists": "Plugin Does Not Exist", "error.pluginNotSupportPlatform": "Plugin Does Not Support Current Platform", "error.pluginReleaseDocFormatError": "Plugin Release Document Format Error", "error.pluginReleaseDocNotFound": "Plugin Release Document Not Found", "error.pluginVersionNotMatch": "FocusAny Version Does Not Meet Plugin Requirements", "error.publishVersionNotMatch": "Plugin Version Mismatch", "error.allFieldsRequired": "All fields are required", "error.archMismatch": "Chip architecture mismatch", "error.avatarModelNotStarted": "Digital human model not started", "error.cancelTaskFailed": "Cancel task failed", "error.energyInsufficient": "Insufficient LLM energy, please recharge to continue using", "error.fileExists": "File already exists", "error.fileNotFound": "File not found", "error.fileSelectFailed": "File selection failed", "error.genCountRange": "Generate count must be between 1-10", "error.loadRecordFailed": "Failed to load record {error}", "error.maxSelection": "Can only select up to {count}", "error.modelArchMismatch": "Model architecture mismatch", "error.modelDirIdentifyFailed": "Model directory identification failed, please select the correct model directory", "error.modelPathInvalid": "Model path cannot contain non-English characters, spaces, etc.", "error.modelPlatformMismatch": "Model platform mismatch", "error.modelServiceNotRunning": "Model service is not running", "error.modelTypeInvalid": "Model type error", "error.modelUnsigned": "Since the model file is not completely signed, please run the following command to complete the signature before running", "error.modelVersionExists": "Model version already exists", "error.nameDuplicate": "Name duplicate", "error.noMicrophone": "No recording device detected", "error.parseFailed": "Failed to parse return data", "error.platformMismatch": "Platform mismatch", "error.processError": "Processing error", "error.processTimeout": "Processing timeout", "error.recognitionModelNotStarted": "Voice recognition model not started", "error.recognitionParamInvalid": "Voice recognition parameters incorrect", "error.recordNotFound": "Record not found", "error.requestError": "Request Error", "error.requestFailed": "Request Failed", "error.responseEmpty": "Response data is empty", "error.resumeFailed": "Resume failed", "error.saveFileFailed": "Save file failed: {error}", "error.selectFileFailed": "Select file failed: {error}", "error.softwareVersionMismatch": "Software does not meet model version requirements", "error.soundAsrResultEmpty": "SoundAsr recognition result is empty, please check if the audio file is normal", "error.soundGenerateResultEmpty": "SoundGenerate generation result is empty, please check if the parameters are correct", "error.textToImageResultEmpty": "TextToImage generation result is empty, please check if the parameters are correct", "error.imageToImageResultEmpty": "ImageToImage generation result is empty, please check if the parameters are correct", "error.taskFailed": "Task failed", "error.taskNotFound": "Task not found", "error.getTaskFailed": "Failed to retrieve task", "error.timbreNotFound": "Voice timbre not found", "error.updateFailed": "Update failed", "error.videoProcessFailed": "Video processing failed, please select another video", "error.voiceModelNotStarted": "Voice model not started", "error.voiceParamInvalid": "Voice synthesis parameters incorrect", "feedback.anytime": "Feedback anytime if you encounter problems", "feedback.help": "Encountered problems? Post for help", "feedback.toolRequest": "Tool Request", "fastPanel.shortcuts": "Quick Actions", "form.inputField": "Please input {title}", "form.required": "{title} is required", "group.name": "Group Name", "guide.audioReq1": "1. Please record in a quiet environment to avoid noise interference", "guide.audioReq2": "2. Please use standard pronunciation, clear articulation, and appropriate speed", "guide.audioReq3": "3. Recording duration is best controlled between 6-20 seconds, and should not exceed 20 seconds", "guide.audioReq4": "4. After recording, listen to check if it meets the requirements before submitting", "guide.videoReq1": "1. The video duration should be between 10 and 30 seconds, in MP4 format, and the recommended resolution is 1080p to 4K", "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", "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", "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", "help.howToAddModel": "How to add a model?", "home.welcome": "Welcome to", "hotkey.doubleClick": " Double Click", "hotkey.instructions": "Instructions: ", "hotkey.notSet": "Not Set", "hotkey.step1": "① Click to activate", "hotkey.step2Mac": "② Press modifier key (Control, Command, Option) first then press other keys, or quickly press modifier key twice", "hotkey.step2Win": "② Press modifier key (Ctrl, Shift, Alt) first then press other keys, or quickly press modifier key twice", "hint.addContent": "Please add content", "hint.audioFormat": "Supports wav/mp3 format", "hint.configPromptFirst": "Please configure prompt first", "hint.fileTypes": "Supported file types", "hint.inputContent": "Please input content", "hint.inputKeywords": "Please input keywords", "hint.inputName": "Please input name", "hint.inputPreviewText": "Input preview text", "hint.inputRandomText": "Please input random speech text", "hint.inputRefText": "Please input reference text", "hint.inputRequirement": "Please input your requirement", "hint.inputStandardText": "Please input standard speech text", "hint.inputSynthesisContent": "Please input synthesis content", "hint.inputTagEnter": "Enter after inputting tag", "hint.inputVoiceSynthesis": "Input voice content to start synthesis", "hint.recordVoice": "Please record voice", "hint.selectAudioFile": "Please select audio file", "hint.selectAvatar": "Please select avatar", "hint.selectAvatarModel": "Please select digital human model", "hint.selectFileFormat": "Please select {extensions} format file", "hint.selectModel": "Please select model", "hint.selectModelCheck": "Please select model to check", "hint.selectModelFirst": "Please select model first", "hint.selectPlatform": "Please select model platform", "hint.selectRecognitionModel": "Please select voice recognition model", "hint.selectSynthesisType": "Please select synthesis type", "hint.selectTimbre": "Please select timbre", "hint.selectVideo": "Please select video", "hint.selectVideoFile": "Please select video file", "hint.selectVoice": "Please select voice", "hint.selectVoiceModel": "Please select voice model", "intro.interactionSupport": "Interactive communication supports major platforms", "intro.lipSync": "Supports audio-driven lip sync replacement", "intro.modelsSupported": "Supported by thousands of timbre models", "intro.modelsUpdate": "Various open-source models continuously updated", "intro.textToVideo": "Input text to automatically synthesize audio driving lip sync to synthesize video", "intro.voiceClone": "Supports built-in voice synthesis, 5-second audio voice cloning", "live.knowledge": "Live Knowledge", "live.knowledgeUpdateHint": "Knowledge base updated, live data will be updated in 30 seconds", "live.knowledgeUpdated": "Live knowledge base updated", "live.noAvatarSelected": "No avatar selected for playback", "live.noLoopMaterialSelected": "No loop material selected", "live.setLiveRoomAddressFirst": "Please set live room address first", "live.live": "Live", "log.autoScroll": "Auto Scroll", "log.view": "Log View", "mcp.noTools": "No tools available", "mcp.serverAddress": "MCP Server Address", "media.audioFile": "Audio File", "media.cropAudio": "Crop Audio", "media.cropConfirm": "Confirm Crop", "media.selectAudio": "Select Audio File", "media.selectVideo": "Select Video File", "media.subtitle": "Subtitle", "media.subtitlePreview": "Subtitle Preview", "media.video": "Video", "monitor.debug": "Debug", "monitor.refresh": "Refresh", "model.accelerationOn": "Continuous call acceleration is on", "model.embedModels": "Embedding Models", "model.freeModels": "Free Models", "model.builtinModels": "Built-in Models", "model.testPrompt": "What model are you, brief answer", "model.add": "Add Model", "model.addCloud": "Add Cloud Model", "model.addLocal": "Add Local Model", "model.addProvider": "Add Provider", "model.addSuccess": "Model added successfully", "model.builtinDesc": "Built-in models can be used directly without configuration", "model.censorResult": "Filtered word detection result", "model.cloudAvatar": "Cloud Avatar", "model.cloudModel": "Cloud Model", "model.cloudModelDesc": "Cloud models support direct use, no need to download and install, convenient and fast", "model.cloudModelService": "Cloud Model Service", "model.cloudVideoAvatar": "Cloud Video Avatar", "model.cloudVideoAvatarDesc": "Cloud video avatar, supports direct download to local use", "model.deleteConfirm": "Are you sure you want to delete model {title} v{version}?", "model.description": "Model Description", "model.download": "Download Model", "model.edit": "Edit Model", "model.editProvider": "Edit Provider", "model.hardwareReq": "Hardware Requirements", "model.id": "Model ID", "model.img2img": "Image-to-Image", "model.info": "Model Info", "model.list": "Model List", "model.localModel": "Local Model", "model.market": "Model Market", "model.marketTip": "Visit Model Market, download model to local", "model.model": "Model", "model.name": "Model Name", "model.notSupported": "Model Not Supported", "model.runInCloudDesc": "Model runs in the cloud, avoiding local resource shortage", "model.runInLocalDesc": "Model runs locally, requires computer performance", "model.searchPlatform": "Search Model Platform", "model.seedTip": "Click to generate random seed, same seed generates same result", "model.select": "Select Model", "model.selectLocal": "Select Local Model", "model.signature": "Model File Signature", "model.systemPrompt": "System Prompt", "model.txt2img": "Text-to-Image", "model.userPrompt": "User Prompt", "model.unzipTip": "Unzip the model archive, select the config.json file in the directory", "model.versionReq": "Version Requirement", "msg.copiedToClipboard": "Copied {text} to clipboard", "msg.fileSavedTo": "File saved to {path}", "msg.moreContent": "For more content, please view", "msg.moreTools": "Submit more tool requests to us", "msg.passwordRequired": "Running process may require password input", "msg.requestSuccessPlaying": "Request successful, start playing", "msg.stopRequested": "Stop request sent, waiting for operation to stop", "msg.videoProcessing": "Video processing may take a long time, please wait patiently", "nav.apps": "Apps", "nav.feedback": "Feedback", "nav.guide": "Guide", "nav.home": "Home", "nav.log": "Log", "nav.toolbox": "Toolbox", "nav.userCenter": "User Center", "payment.error": "Error occurred", "payment.expired": "Expired", "payment.paidClosing": "Paid, closing soon", "payment.payWithinSeconds": "Pay within {seconds} seconds", "payment.qrcodeExpired": "QR Code Expired", "payment.scanQRCode": "Scan with WeChat / Alipay", "payment.scanned": "Scanned", "placeholder.chatgpt": "e.g. ChatGPT", "placeholder.gpt35": "e.g. GPT-3.5", "placeholder.requiredGpt": "Required, e.g. gpt-3.5-turbo", "plugin.autoDetachWindow": "Auto Detach to Independent Window", "plugin.backendLog": "Plugin Backend Log", "plugin.debugWindow": "Plugin Debug Window", "plugin.disabled": "Disabled", "plugin.enabled": "Enabled", "plugin.installFailed": "Install Failed", "plugin.installLocalConfig": "Select Plugin config.json", "plugin.installLocalZip": "Select Local ZIP Plugin", "plugin.installSuccess": "Install Success", "plugin.localPlugin": "Local Plugin:{path}", "plugin.market": "Plugin Market", "plugin.notFound": "No plugins found", "plugin.publish": "Publish Plugin", "plugin.publishFailed": "Publish failed:{error}", "plugin.publishSuccess": "Publish successful", "plugin.publishing": "Publishing", "plugin.refreshSuccess": "Refresh successful", "plugin.search": "Search Plugin", "plugin.uninstall": "Uninstall", "plugin.uninstallConfirm": "Are you sure you want to uninstall the plugin?", "plugin.uninstallFailed": "Uninstall failed:{error}", "plugin.uninstallSuccess": "Uninstall successful", "plugin.updateInfo": "Update Info", "plugin.updateInfoFailed": "Update Info Failed", "plugin.updateInfoSuccess": "Update Info Success", "plugin.updatingInfo": "Updating Info", "proUpgrade.defaultDesc": "Please download Pro version to unlock full features", "proUpgrade.downloadButton": "Download Pro Version", "proUpgrade.title": "Upgrade Feature Tip", "provider.baichuan": "Baichuan", "provider.baiduCloud": "Baidu Cloud Qianfan", "provider.buildIn": "Built-in Models", "provider.dashscope": "Alibaba Cloud Bailian", "provider.deepseek": "DeepSeek", "provider.doubao": "Volcengine", "provider.hunyuan": "Tencent Hunyuan", "provider.infini": "Infini-AI", "provider.modelscope": "ModelScope", "provider.moonshot": "Moonshot AI", "provider.nvidia": "NVIDIA", "provider.ppio": "PPIO", "provider.silicon": "Silicon Flow", "provider.stepfun": "StepFun", "provider.tencentCloudTi": "Tencent Cloud TI", "provider.xirang": "Tianyiyun Xirang", "provider.yi": "Yi (01.AI)", "provider.zhinao": "360 AI", "provider.zhipu": "Zhipu AI", "service.start": "Start Service", "service.startFailed": "Start Failed", "service.starting": "Starting", "setting.altDoubleClick": "Alt Double Click", "setting.altSingleClick": "Alt Single Click", "setting.apiKey": "API Key", "setting.apiUrl": "API URL", "setting.askEveryTime": "Ask Every Time", "setting.autoLaunch": "Launch at Startup", "setting.autoStart": "Auto Start", "setting.autoUpdate": "Auto Check Update", "setting.basic": "Basic Settings", "setting.commandDoubleClick": "Command Double Click", "setting.commandSingleClick": "Command Single Click", "setting.controlDoubleClick": "Control Double Click", "setting.controlSingleClick": "Control Single Click", "setting.ctrlDoubleClick": "Ctrl Double Click", "setting.ctrlSingleClick": "Ctrl Single Click", "setting.cudaAcceleration": "CUDA Acceleration", "setting.dataConfig": "Data Config", "setting.detachWindowHotkey": "Detach Window Hotkey", "setting.env": "Environment Settings", "setting.exitDirectly": "Exit Directly", "setting.fixCommand": "Fix Command", "setting.followSystem": "Follow System", "setting.interfaceType": "Interface Type", "setting.llm": "LLM Settings", "setting.localModelDir": "Local Model Dir", "setting.onClose": "On Close", "setting.optionDoubleClick": "Option Double Click", "setting.optionSingleClick": "Option Single Click", "setting.pathChangeConfirm": "Confirm change storage path to {path}?", "setting.pathChangeRestart": "Changing storage path requires restarting software", "setting.storagePath": "Storage Path", "setting.themeStyle": "Theme Style", "setting.triggerType": "Trigger Type", "setting.wpm": "Words Per Minute", "setup.allCompleted": "All setup completed", "setup.congratulations": "Congratulations on completing {title} setup", "setup.openSettings": "Open Settings", "setup.verifyComplete": "Verify Complete", "soundAsr.copyResult": "Copy Recognition Result", "soundAsrEdit.inputSearchContent": "Please input search content", "soundAsrEdit.invalidTimeRange": "Invalid time range, must be within the record", "soundAsrEdit.mergedBlankSegments": "Merged continuous blank segments", "soundAsrEdit.mergeOnlyContinuous": "Can only merge continuous records", "soundAsrEdit.noEditRecord": "No edit record", "soundAsrEdit.noMatchFound": "No match found", "soundAsrEdit.optimizeComplete": "Optimization completed, successfully fixed {successCount} sentences, failed {failCount} sentences", "soundAsrEdit.replacedRecords": "Replaced {count} records", "soundAsrEdit.time": "Time", "soundReplace.confirmComplete": "Confirm Completed", "soundReplace.confirmText": "Confirm Text", "soundReplace.confirmTextDesc": "Check and confirm the recognized text content", "soundReplace.extractAndRecognize": "Extract and Recognize Audio", "soundReplace.extractAndRecognizeDesc": "Select video file containing voice to replace", "soundReplace.extractAudio": "Extract Audio", "soundReplace.manualConfirmText": "Manually Confirm Text", "soundReplace.modifyText": "Modify Text", "soundReplace.reorderConfirm": "Reorder Confirm", "soundReplace.reverifyText": "Reverify Text", "soundReplace.saveAndSynthesize": "Save and Synthesize", "soundReplace.submitTask": "Submit Task", "soundReplace.synthesizeReplace": "Synthesize and Replace Voice", "soundReplace.synthesizeReplaceDesc": "Set voice synthesis model parameters to generate new speech", "soundReplace.taskSubmitted": "Task submitted", "soundReplace.videoSynthesis": "Video Synthesis", "status.cancelling": "Cancelling", "status.deleting": "Deleting", "status.downloading": "Downloading", "status.downloadingProgress": "Downloading {index}/{total}", "status.loading": "Loading", "status.manuallyCompleted": "Manually Completed", "status.notRunning": "Not Running", "status.queuing": "Queuing", "status.resuming": "Resuming", "status.retrying": "Retrying", "status.running": "Running", "status.startedTime": "Started {time}", "status.stopped": "Stopped", "status.submitting": "Submitting", "status.unprocessed": "Unprocessed", "status.waiting": "Waiting", "subtitleTts.audioSynthesis": "Audio Synthesis", "subtitleTts.parseSubtitle": "Parse Subtitle", "subtitleTts.settings": "Subtitle to Audio Settings", "subtitleTts.synthesizedAudio": "Synthesized Audio", "store.searchPlaceholder": "Enter keywords to search", "system.actionManagement": "Action Management", "system.addFileLaunch": "Add a File Launch", "system.aiModel": "AI Models", "system.dataCenter": "Data Center", "system.fileLaunch": "File Launch", "system.functionSettings": "Function Settings", "system.hotkeys": "Hotkeys", "system.myAccount": "My Account", "system.personalCenter": "Personal Center", "system.pluginManagement": "Plugin Management", "system.preferences": "Preferences", "task.batchTextSynthesis": "Batch Text Synthesis", "task.cancel": "Cancel Task", "task.cancelled": "Task Cancelled", "task.cloneSubmitted": "Task submitted successfully, waiting for cloning", "task.details": "Task Details", "task.editResult": "Edit Result", "task.longTextToAudio": "Long Text to Audio", "task.oneClickRun": "One Click Run", "task.optimizeTimeline": "One Click Optimize Timeline", "task.processing": "Processing", "task.recognitionSubmitted": "Voice recognition task submitted", "task.resume": "Resume Task", "task.retry": "Retry Task", "task.startRecognition": "Start Recognition", "task.startSynthesis": "Start Synthesis", "task.startVideoGen": "Start Video Generation", "task.submitSynthesis": "Submit Synthesis", "task.subtitleToAudio": "Subtitle to Audio", "task.synthesisType": "Synthesis Type", "task.synthesize": "Synthesize", "task.videoGenSubmitted": "Task submitted successfully, waiting for video generation", "task.view": "View Task", "theme.dark": "Dark", "theme.light": "Light", "time.hour": "Hour", "time.minute": "Minute", "time.second": "Second", "update.alreadyLatest": "Already latest version", "update.check": "Check Update", "update.checkFailed": "Check Update Failed", "update.newVersionFound": "New version {version} found, download and update now?", "user.comment": "User Comment", "user.energy": "Energy", "user.enter": "User Enter", "user.knowledge": "User Knowledge", "user.like": "User Like", "user.name": "Username", "user.reward": "User Reward", "video.contentVideo": "Content Video", "video.loopBroadcast": "Loop Broadcast", "video.loopContent": "Loop Content Video", "video.providerName": "Provider Name", "voice.add": "Add Voice", "voice.clone": "Voice Clone", "voice.cloneModel": "Voice Clone Model", "voice.config": "Voice Config", "voice.crossLanguage": "Cross Language", "voice.currentConfig": "Current Voice Config", "voice.file": "Voice File", "voice.recognition": "Voice Recognition", "voice.recognitionConfig": "Voice Recognition Config", "voice.recognitionModel": "Voice Recognition Model", "voice.record": "Record Audio", "voice.refAudioGuide1": "Reference audio controlled within 6-20s to ensure audio clarity", "voice.refAudioGuide2": "Reference audio needs to be > 6s and < 20s to ensure clarity", "voice.refTextRequired": "Reference audio full text content required, some models need it", "voice.referenceAudio": "Reference Audio", "voice.referenceText": "Reference Text", "voice.replace": "Voice Replace", "voice.replaceConfig": "Voice Replace Config", "voice.rerecord": "Re-record", "voice.select": "Select Voice", "voice.selectFile": "Select Voice File", "voice.selectTimbre": "Select Timbre", "voice.synthesis": "Voice Synthesis", "voice.synthesisConfig": "Voice Synthesis Config", "voice.synthesisModel": "Voice Synthesis Model", "voice.timbre": "Voice Timbre", "voice.timbreDesc": "Timbre Description", "voice.timbreManage": "Timbre Manage", "voice.voice": "Voice", "welcome.title": "Welcome to AIGCPanel!", "workflow.create": "Create Workflow", "workflow.workflow": "Workflow", "workflow.configureRecognitionAndGeneration": "Please configure voice recognition and voice generation services", "workflow.inputVideoParam": "Please input video parameter", "workflow.paramErrorMissing": "Parameter error: missing {items}", "workflow.soundGenerationService": "Voice generation service", "workflow.soundRecognitionService": "Voice recognition service", "about.devModeSettings": "Dev Mode Settings", "about.fastPanelHideOnBlur": "Fast Panel Hide on Blur", "action.backendCode": "Backend Code", "action.code": "Code", "action.command": "Command", "action.smartArea": "Smart Area", "action.webpage": "Webpage", "backup.connectFailed": "Connection Failed", "backup.fileFormat": "File Format", "backup.notConfigured": "WebDav not configured, click to configure", "backup.password": "Password", "backup.placeholderSupport": "Placeholder Support", "backup.rootDir": "Root Directory", "backup.selectFile": "Select file to restore", "backup.selectRestoreFile": "Please select a file to restore", "backup.startBackup": "Start Backup", "backup.startRestore": "Start Restore", "backup.uploadToCloud": "Upload to Cloud", "backup.restoreFromCloud": "Restore from Cloud", "backup.username": "Username", "backup.webdavConfig": "Config", "backup.webdavSettings": "WebDav Settings", "data.backupRestore": "Backup/Restore", "data.clear": "Clear", "data.clearConfirm": "Are you sure to clear all?", "data.clearSuccess": "Cleared successfully", "data.deleteConfirm": "Are you sure to delete?", "data.deleteSuccess": "Deleted successfully", "data.docCount": "documents", "data.filterPlaceholder": "Type keyword to filter", "data.title": "Data Center", "launch.actionName": "Action name, e.g. Screenshot", "launch.addHotkey": "Add Hotkey", "launch.custom": "Custom", "launch.enterActionName": "Please enter action name", "launch.hotkey": "Hotkey", "log.noLogs": "No Log Files", "log.openFile": "Open File", "main.clearAllConfirm": "Confirm clear all?", "main.expandAll": "Expand All", "main.loading": "Loading", "main.matchResults": "Match Results", "main.multipleFiles": "Multiple Files", "main.multipleFolders": "Multiple Folders", "main.multipleImages": "Multiple Images", "main.pinned": "Pinned", "main.placeholder": "FocusAny, make your work focused and efficient", "main.recentlyUsed": "Recently Used", "main.runError": "An error occurred during execution", "main.searchResults": "Search Results", "main.starting": "Starting", "main.window": "Window", "plugin.detachWindow": "Open in Detached Window", "plugin.actionNotFound": "Action not found, please check keywords or action name", "plugin.colorCopied": "Color {color} copied to clipboard", "plugin.colorCopyShortcut": "Copy {shortcut}", "plugin.errorLog": "Plugin {name} error: {error}", "plugin.exitEsc": "Exit ESC", "plugin.installComplete": "Plugin {title} installed successfully", "plugin.installing": "Installing plugin", "plugin.newWindow": "New Window", "plugin.noPermission": "Plugin has no permission ({permission})", "plugin.opening": "Opening plugin", "plugin.notExist": "Plugin {name} not found", "plugin.screenshotHint": "Please use the screenshot tool", "plugin.selectSavePath": "Select save path", "editor.noPluginForFile": "No plugin found to open this file", "file.notFoundOrReadFailed": "File not found or read failed", "file.unsupportedType": "Unsupported file type", "screenshot.edit": "Screenshot Editor", "system.title": "System Settings", "system.desc": "Provides basic system functions", "system.apps": "Applications", "system.appsDesc": "Search and open system applications", "system.appsIndexed": "Application indexing complete", "system.appsIndexing": "Analyzing applications, search will be available soon...", "system.fileLaunchDesc": "One-click file launch", "system.workflowDesc": "Workflow management", "system.storeDesc": "Plugin market management", "system.about": "About Us", "system.screenshot": "Screenshot", "system.colorPicker": "Color Picker", "system.screenRecord": "Screen Recording", "system.lockScreen": "Lock Screen", "system.lanIP": "LAN IP", "system.ipCopied": "IP address {ip} copied to clipboard", "tray.visitWebsite": "Visit Website", "debug.info": "Debug Info", "debug.copyRoute": "Copy Route", "setting.darkTheme": "Dark", "setting.fastPanel": "Fast Panel", "setting.fastPanelHotkey": "Fast Panel Hotkey", "setting.functionSettings": "Function Settings", "setting.invokeHotkey": "Invoke Hotkey", "setting.language": "Interface Language", "setting.lightTheme": "Light" } ================================================ FILE: src/lang/index.ts ================================================ import { createI18n } from "vue-i18n"; import enUS from "./en-US.json"; import zhCN from "./zh-CN.json"; let localeInit = false; export const defaultLocale = "zh-CN"; export const messageList = [ { name: "en-US", label: "English", messages: enUS, }, { name: "zh-CN", label: "简体中文", messages: zhCN, }, ]; const buildMessages = (): any => { let messages = {}; for (let m of messageList) { messages[m.name] = m.messages; } return messages; }; const messages = buildMessages(); export const i18n = createI18n({ locale: defaultLocale, legacy: false, globalInjection: true, messages, }); if (typeof window !== "undefined" && window.$mapi) { window.$mapi.config.get("lang", defaultLocale).then((lang: string) => { i18n.global.locale.value = lang as any; localeInit = true; fireLocaleChange(lang); }); } export type LocaleItem = { name: string; label: string; active?: boolean; }; export const listLocales = () => { let list: LocaleItem[] = messageList; list.forEach((item) => { item.active = i18n.global.locale.value === item.name; }); return list; }; export const getLocale = async () => { return new Promise((resolve) => { if (localeInit) { resolve(i18n.global.locale.value); } else { setTimeout(() => { resolve(getLocale()); }, 100); } }); }; let localeChangeListener: Array<(locale: string) => void> = []; export const onLocaleChange = (callback: (lang: string) => void) => { localeChangeListener.push(callback); }; const fireLocaleChange = (lang: string) => { localeChangeListener.forEach((callback) => { callback(lang); }); }; export const changeLocale = (lang: string) => { i18n.global.locale.value = lang as any; window.$mapi.config.set("lang", lang).then(() => { fireLocaleChange(lang); }); }; export const applyLocale = (lang: string) => { i18n.global.locale.value = lang as any; fireLocaleChange(lang); }; export const t = (key: string, param: object | null = null) => { // check if exists key if (!(key in messages[i18n.global.locale.value])) { if (param) { return key.replace(/\{(\w+)\}/g, function (match, key) { return key in param ? param[key] : match; }); } return key; } // @ts-ignore return i18n.global.t(key, param as any); }; ================================================ FILE: src/lang/zh-CN.json ================================================ { "config.slogan": "专注提效的AI工具条", "model.testPrompt": "你是什么模型,简短回答", "about.disclaimer": "声明", "about.license": "本产品为开源软件,遵循 AGPL-3.0 license 协议。", "about.software": "关于软件", "about.title": "关于", "action.attrMatch": "属性匹配", "action.builtin": "内置", "action.fileExtensions": "文件后缀", "action.fileType": "文件类型", "action.matchAction": "匹配动作", "action.matchEditorHint": "当文件匹配到以下后缀和类型时", "action.matchExactKeyHint": "输入完全等于以下关键词", "action.matchFileHint": "当使用以下规则匹配成功时", "action.matchImageHint": "当匹配到图片时", "action.matchKeywordHint": "输入匹配以下关键词,包含全拼、首字母简写", "action.matchRegexHint": "输入匹配以下正则表达式", "action.matchWindowHint": "当激活窗口匹配以下条件成功时", "action.maxCount": "最大数量", "action.minCount": "最小数量", "action.nameMatch": "名称匹配", "action.pinToSearch": "固定到搜索框", "action.plugin": "插件", "action.regex": "正则", "action.searchAction": "搜索动作", "action.suffix": "后缀", "action.titleMatch": "标题匹配", "action.unpinFromSearch": "从搜索框取消固定", "action.window": "窗口", "avatar.addVideo": "添加视频形象", "avatar.audioToVideo": "音频驱动口型合成视频", "avatar.avatar": "数字人形象", "avatar.canOpenCloseMouth": "可张口闭口", "avatar.config": "数字人配置", "avatar.digitalHuman": "数字人", "avatar.example": "形象示例", "avatar.faceInterference": "面部有干扰", "avatar.live": "数字人直播", "avatar.manage": "管理多个数字人形象", "avatar.model": "数字人模型", "avatar.oneClickSynthesis": "数字人一键合成", "avatar.saveToMine": "保存到我的形象", "avatar.selfie": "正脸自拍", "avatar.smartLive": "智能直播", "avatar.synthesis": "数字人合成", "avatar.video": "视频形象", "avatar.videoReq": "形象视频要求", "backup.backingUp": "正在备份...", "backup.backupFailed": "备份失败", "backup.backupSuccess": "备份成功", "backup.backupToFile": "备份为文件", "backup.backupToLocal": "备份到本地", "backup.formatTip": "备份采用 backup 格式,定期备份文件可避免数据丢失。", "backup.restoreFromFile": "从文件恢复", "backup.restoreFromLocal": "从本地恢复", "backup.restoreFailed": "恢复失败", "backup.restoreSuccess": "恢复成功", "backup.restoring": "正在恢复...", "backup.title": "备份/恢复", "common.adapt": "适配", "common.add": "添加", "common.addFile": "添加文件", "common.addOne": "添加一个", "common.aiGenerated": "AI生成", "common.all": "全部", "common.and": "和", "common.availableVars": "可用变量", "common.back": "返回", "common.batchInput": "批量输入", "common.batchPaste": "批量粘贴", "common.batchPasteHint": "批量粘贴,每行一个", "common.cancel": "取消", "common.check": "检查", "common.clearConfirm": "确认清空?", "common.clearHistory": "清空历史", "common.clickTextToCopy": "点击文字复制", "common.clickToConfig": "点击配置", "common.clickToCopy": "点击复制", "common.close": "关闭", "common.collapse": "收起", "common.confirm": "确定", "common.copySuccess": "复制成功", "common.copyText": "复制文本", "common.default": "默认", "common.disable": "禁用", "common.enable": "启用", "common.open": "打开", "common.delete": "删除", "common.deleteConfirm": "确认删除?", "common.deleteRecordsConfirm": "确定删除 {count} 条记录?", "common.description": "说明", "common.detail": "详情", "common.docs": "文档", "common.download": "下载", "common.downloadFailed": "下载失败", "common.downloadSuccess": "下载成功", "common.downloadingWait": "下载中,请耐心等待", "common.duration": "时长", "common.edit": "编辑", "common.error": "错误", "common.exit": "退出", "common.exitConfirm": "确定退出软件?", "common.expand": "展开", "common.extensions": "{extensions}", "common.failed": "失败", "common.feature": "功能", "common.file": "文件", "common.find": "查找", "common.folder": "文件夹", "common.generate": "生成", "common.generateFailed": "生成失败", "common.hideWindow": "隐藏窗口", "common.image": "图片", "common.info": "信息", "common.inputContent": "输入内容", "common.key": "键", "common.keywords": "关键词", "common.language": "语言", "common.loading": "加载中", "common.loadingDots": "加载中...", "common.localFile": "本地文件", "common.loginRequired": "请先登录", "common.merge": "合并", "common.mergeWhitespace": "合并空白", "common.modify": "修改", "common.more": " 更多", "common.moreDetails": "获取更多详情", "common.name": "名称", "common.no": "否", "common.none": "无", "common.notLoggedIn": "未登录", "common.officialSite": "官网", "common.onlineDocs": "在线文档", "common.openFile": "打开文件", "common.openPath": "打开路径", "common.pause": "暂停", "common.pro": "Pro", "common.recharge": "充值", "common.refresh": "刷新", "common.rememberChoice": "记住我的选择", "common.replace": "替换", "common.reselect": "重新选择", "common.resolution": "分辨率", "common.restoreDefault": "恢复默认", "common.resumeSuccess": "继续成功", "common.retryAttempt": "继续尝试", "common.retryFailed": "重试失败", "common.retrySuccess": "重试成功", "common.save": "保存", "common.saveSuccess": "保存成功", "common.select": "选择", "common.selectAll": "全选", "common.selectFile": "选择文件", "common.selectLocalFile": "选择本地文件", "common.selectPath": "选择路径", "common.sendFailed": "发送失败", "common.sendSuccess": "发送成功", "common.service": "服务", "common.setting": "设置", "common.settingSuccess": "设置成功", "common.split": "分割", "common.stopFailed": "停止失败", "common.stopService": "停止服务", "common.stopping": "停止中", "common.submitConfirm": "确认提交", "common.success": "成功", "common.system": "系统", "common.tag": "标签", "common.test": "测试", "common.testFailed": "测试失败", "common.testSuccess": "测试成功", "common.testing": "测试中,请稍候...", "common.tip": "提示", "common.totalCount": "共 {count} 条", "common.totalWords": "共{count}字", "common.type": "类型", "common.useNow": "立即使用", "common.value": "值", "common.version": "版本", "common.view": "查看", "common.viewCode": "代码查看", "common.viewEffect": "效果查看", "common.viewRecord": "记录查看", "common.vipRequired": "请先开通会员", "common.yes": "是", "dashboard.statistics": "数据统计", "dashboard.today": "今日", "dashboard.todayTotalTasks": "今日总任务", "desc.img2img": "根据输入图片+描述提示生成新的图片", "desc.longTextToAudio": "将长文本内容转换为音频文件", "desc.recognitionDownload": "识别文件下载文本/字幕", "desc.recognitionEdit": "识别音频文件,支持识别后编辑/下载文本/字幕文件", "desc.subtitleToAudio": "将字幕文件转换为音频文件", "desc.txt2img": "根据文本描述生成图片", "desc.videoVoiceReplace": "将视频中的人声替换为其他音色", "download.audio": "下载音频", "download.subtitleFile": "下载字幕文件", "download.textFile": "下载文本文件", "empty.noDownloadRecord": "没有可以下载的记录", "empty.noEditableData": "没有可编辑的数据", "empty.noLocalModel": "没有可用本地模型", "empty.noLog": "暂无日志", "empty.noLogFile": "暂无日志文件", "empty.noModel": "没有可用模型", "empty.noModelAdd": "暂时还没有模型,请添加模型~", "empty.noModelPlatform": "没有找到相关模型平台", "empty.noRecognitionTask": "暂无语音识别任务", "empty.noRecord": "没有可用记录", "empty.noVoiceTask": "暂无声音合成任务", "error.pluginAlreadyExists": "插件已存在", "error.pluginEditionNotMatch": "FocusAny类型不满足插件要求", "error.pluginFormatError": "插件格式错误", "error.pluginNotExists": "插件不存在", "error.pluginNotSupportPlatform": "插件不支持当前平台", "error.pluginReleaseDocFormatError": "插件release文档格式错误", "error.pluginReleaseDocNotFound": "插件release文档不存在", "error.pluginVersionNotMatch": "FocusAny版本不满足插件要求", "error.publishVersionNotMatch": "插件版本不匹配", "error.allFieldsRequired": "所有内容不能为空", "error.archMismatch": "芯片架构不匹配", "error.avatarModelNotStarted": "数字人模型未启动", "error.cancelTaskFailed": "取消任务失败", "error.energyInsufficient": "大模型能量不足,请充值后继续使用", "error.fileExists": "文件已存在", "error.fileNotFound": "文件未找到", "error.fileSelectFailed": "文件选择失败", "error.genCountRange": "生成数量必须在1-10之间", "error.loadRecordFailed": "加载记录失败 {error}", "error.maxSelection": "最多只能选择{count}个", "error.modelArchMismatch": "模型架构不匹配", "error.modelDirIdentifyFailed": "模型目录识别失败,请选择正确的模型目录", "error.modelPathInvalid": "模型路径不能包含非英文、空格等特殊字符", "error.modelPlatformMismatch": "模型平台不匹配", "error.modelServiceNotRunning": "模型服务未运行", "error.modelTypeInvalid": "模型类型错误", "error.modelUnsigned": "由于模型文件未完全签名,请运行以下命令完成签名后运行", "error.modelVersionExists": "模型相同版本已存在", "error.nameDuplicate": "名称重复", "error.noMicrophone": "未检测到录音设备", "error.parseFailed": "解析返回数据失败", "error.platformMismatch": "平台不匹配", "error.processError": "处理出错", "error.processTimeout": "处理超时", "error.recognitionModelNotStarted": "语音识别模型未启动", "error.recognitionParamInvalid": "语音识别参数不正确", "error.recordNotFound": "未找到记录", "error.requestError": "请求错误", "error.requestFailed": "请求失败", "error.responseEmpty": "返回数据为空", "error.resumeFailed": "继续失败", "error.saveFileFailed": "保存文件失败: {error}", "error.selectFileFailed": "选择文件失败:{error}", "error.softwareVersionMismatch": "软件不满足模型版本要求", "error.soundAsrResultEmpty": "SoundAsr 识别结果为空,请检查音频文件是否正常", "error.soundGenerateResultEmpty": "SoundGenerate 生成结果为空,请检查参数是否正确", "error.textToImageResultEmpty": "TextToImage 生成结果为空,请检查参数是否正确", "error.imageToImageResultEmpty": "ImageToImage 生成结果为空,请检查参数是否正确", "error.taskFailed": "任务失败", "error.taskNotFound": "任务不存在", "error.getTaskFailed": "任务获取失败", "error.timbreNotFound": "声音音色不存在", "error.updateFailed": "更新失败", "error.videoProcessFailed": "视频处理失败,请选择其他视频", "error.voiceModelNotStarted": "声音模型未启动", "error.voiceParamInvalid": "声音合成参数不正确", "feedback.anytime": "遇到问题随时反馈", "feedback.help": "使用遇到问题?发帖求助", "feedback.toolRequest": "工具需求", "fastPanel.shortcuts": "快捷动作", "form.inputField": "请输入{title}", "form.required": "{title}不能为空", "group.name": "分组名称", "guide.audioReq1": "1. 请在安静的环境下进行录音,避免噪音干扰", "guide.audioReq2": "2. 请使用标准普通话,吐字清晰,语速适当", "guide.audioReq3": "3. 录音时长控制在 6~20秒 最佳,最多不超过20秒", "guide.audioReq4": "4. 录制完成后先试听看是否达到要求再提交", "guide.videoReq1": "1. 视频时长要求在10秒~30秒,视频格式为MP4,建议分辨率1080p~4K", "guide.videoReq2": "2. 为保障效果,视频必须保证每一帧都要正面露脸,脸部无任何遮挡,并且视频中只能出现同一个人脸", "guide.videoReq3": "3. 视频人物建议闭口或微微张口,张口幅度不宜过大,距离镜头一定距离,可根据合成效果自行调整", "guide.videoReq4": "4. 不能全程闭嘴,可以正常语气循环说 一二三四五六七八九 等文字", "help.howToAddModel": "如何添加模型?", "home.welcome": "欢迎使用", "hotkey.doubleClick": "双击", "hotkey.instructions": "使用方式:", "hotkey.notSet": "未设置", "hotkey.step1": "① 点击激活", "hotkey.step2Mac": "② 先按功能键(Control、Command、Option)再按其他普通键,也可快速按快功能键2次", "hotkey.step2Win": "② 先按功能键(Ctrl、Shift、Alt)再按其他普通键,也可快速按快功能键2次", "hint.addContent": "请添加内容", "hint.audioFormat": "支持 wav/mp3 格式", "hint.configPromptFirst": "请先配置提示词", "hint.fileTypes": "支持的文件类型", "hint.inputContent": "请输入内容", "hint.inputKeywords": "请输入关键词", "hint.inputName": "请输入名称", "hint.inputPreviewText": "输入预览文字", "hint.inputRandomText": "请输入随机话术", "hint.inputRefText": "请输入参考文字", "hint.inputRequirement": "请输入你的需求", "hint.inputStandardText": "请输入标准话术", "hint.inputSynthesisContent": "请输入合成内容", "hint.inputTagEnter": "输入标签后回车", "hint.inputVoiceSynthesis": "输入语音内容开始合成", "hint.recordVoice": "请录制声音", "hint.selectAudioFile": "请选择音频文件", "hint.selectAvatar": "请选择形象", "hint.selectAvatarModel": "请选择数字人模型", "hint.selectFileFormat": "请选择{extensions}格式的文件", "hint.selectModel": "请选择模型", "hint.selectModelCheck": "请选择要检测的模型", "hint.selectModelFirst": "请先选择模型", "hint.selectPlatform": "请选择模型平台", "hint.selectRecognitionModel": "请选择语音识别模型", "hint.selectSynthesisType": "请选择合成类型", "hint.selectTimbre": "请选择声音音色", "hint.selectVideo": "请选择视频", "hint.selectVideoFile": "请选择视频文件", "hint.selectVoice": "请选择声音", "hint.selectVoiceModel": "请选择声音模型", "intro.interactionSupport": "互动交流支持各大平台", "intro.lipSync": "支持音频驱动实现口型替换", "intro.modelsSupported": "上千种音色模型支持", "intro.modelsUpdate": "多种开源模型持续更新", "intro.textToVideo": "输入文本自动合成音频驱动口型合成视频", "intro.voiceClone": "支持内置声音合成,5秒音频声音克隆", "live.knowledge": "直播知识", "live.knowledgeUpdateHint": "知识库更新,直播数据将会在30秒后更新", "live.knowledgeUpdated": "直播知识库已更新", "live.noAvatarSelected": "没有选择播放的数字人", "live.noLoopMaterialSelected": "没有选择循环素材", "live.setLiveRoomAddressFirst": "请先设置直播间地址", "live.live": "直播", "log.autoScroll": "自动滚动", "log.view": "日志查看", "mcp.noTools": "暂无可用工具", "mcp.serverAddress": "MCP Server 地址", "media.audioFile": "音频文件", "media.cropAudio": "裁剪音频", "media.cropConfirm": "确定裁剪", "media.selectAudio": "选择音频文件", "media.selectVideo": "选择视频文件", "media.subtitle": "字幕", "media.subtitlePreview": "字幕预览", "media.video": "视频", "monitor.debug": "调试", "monitor.refresh": "刷新", "model.accelerationOn": "连续调用加速已开启", "model.embedModels": "嵌入模型", "model.freeModels": "免费模型", "model.builtinModels": "大模型", "model.add": "添加模型", "model.addCloud": "添加云端模型", "model.addLocal": "添加本地模型", "model.addProvider": "添加供应商", "model.addSuccess": "模型添加成功", "model.builtinDesc": "内置模型无需配置可直接使用", "model.censorResult": "违规词检测结果", "model.cloudAvatar": "云端形象", "model.cloudModel": "云端模型", "model.cloudModelDesc": "云端模型支持直接使用,无需下载和安装,方便快捷", "model.cloudModelService": "云端模型服务", "model.cloudVideoAvatar": "云端视频形象", "model.cloudVideoAvatarDesc": "云端视频形象,支持直接下载到本地使用", "model.deleteConfirm": "确定删除模型 {title} v{version} 吗?", "model.description": "模型说明", "model.download": "下载模型", "model.edit": "编辑模型", "model.editProvider": "编辑供应商", "model.hardwareReq": "硬件要求", "model.id": "模型ID", "model.img2img": "图生图", "model.info": "模型信息", "model.list": "模型列表", "model.localModel": "本地模型", "model.market": "模型市场", "model.marketTip": "访问模型市场,下载模型到本地", "model.model": "模型", "model.name": "模型名称", "model.notSupported": "模型不支持", "model.runInCloudDesc": "模型运行在云端,避免本地资源不足", "model.runInLocalDesc": "模型运行在本地,对电脑性能有要求", "model.searchPlatform": "搜索模型平台", "model.seedTip": "点击生成随机种子,种子相同则生成的结果相同", "model.select": "选择模型", "model.selectLocal": "选择本地模型", "model.signature": "模型文件签名", "model.systemPrompt": "系统提示词", "model.txt2img": "文生图", "model.userPrompt": "用户提示词", "model.unzipTip": "解压模型压缩包,选择目录中的config.json文件", "model.versionReq": "版本要求", "msg.copiedToClipboard": "已复制 {text} 剪切板", "msg.fileSavedTo": "文件已保存到 {path}", "msg.moreContent": "更多内容,请查看", "msg.moreTools": "更多工具提交需求给我们", "msg.passwordRequired": "运行过程可能需要输入密码", "msg.requestSuccessPlaying": "请求成功,开始播放", "msg.stopRequested": "已发送停止请求,请等待运行停止", "msg.videoProcessing": "视频处理可能需要较长时间,请耐心等待", "nav.apps": "应用工具", "nav.feedback": "工单反馈", "nav.guide": "新手指引", "nav.home": "首页", "nav.log": "日志", "nav.toolbox": "工具箱", "nav.userCenter": "用户中心", "payment.error": "出错了", "payment.expired": "已过期", "payment.paidClosing": "已支付,即将关闭", "payment.payWithinSeconds": "{seconds}秒内支付", "payment.qrcodeExpired": "二维码已过期", "payment.scanQRCode": "微信 / 支付宝 扫一扫", "payment.scanned": "已扫码", "placeholder.chatgpt": "例如 ChatGPT", "placeholder.gpt35": "例如 GPT-3.5", "placeholder.requiredGpt": "必填 如 gpt-3.5-turbo", "plugin.autoDetachWindow": "自动分离为独立窗口显示", "plugin.backendLog": "插件后端日志", "plugin.debugWindow": "插件调试窗口", "plugin.disabled": "已禁用", "plugin.enabled": "已启用", "plugin.installFailed": "安装失败", "plugin.installLocalConfig": "选择插件config.json", "plugin.installLocalZip": "选择本地ZIP插件", "plugin.installSuccess": "安装成功", "plugin.localPlugin": "本地插件:{path}", "plugin.market": "插件市场", "plugin.notFound": "没有找到插件", "plugin.publish": "发布插件", "plugin.publishFailed": "发布失败:{error}", "plugin.publishSuccess": "发布成功", "plugin.publishing": "正在发布", "plugin.refreshSuccess": "刷新成功", "plugin.search": "搜索插件", "plugin.uninstall": "卸载", "plugin.uninstallConfirm": "确定要卸载插件吗?", "plugin.uninstallFailed": "卸载失败:{error}", "plugin.uninstallSuccess": "卸载成功", "plugin.updateInfo": "更新信息", "plugin.updateInfoFailed": "更新资料失败", "plugin.updateInfoSuccess": "更新资料成功", "plugin.updatingInfo": "正在更新资料", "proUpgrade.defaultDesc": "请下载 Pro 版本解锁完整功能", "proUpgrade.downloadButton": "立即下载 Pro 版本", "proUpgrade.title": "功能升级提示", "provider.baichuan": "百川", "provider.baiduCloud": "百度云千帆", "provider.buildIn": "大模型", "provider.dashscope": "阿里云百炼", "provider.deepseek": "深度求索", "provider.doubao": "火山引擎", "provider.hunyuan": "腾讯混元", "provider.infini": "无问芯穹", "provider.modelscope": "ModelScope 魔搭", "provider.moonshot": "月之暗面", "provider.nvidia": "英伟达", "provider.ppio": "PPIO 派欧云", "provider.silicon": "硅基流动", "provider.stepfun": "阶跃星辰", "provider.tencentCloudTi": "腾讯云TI", "provider.xirang": "天翼云息壤", "provider.yi": "零一万物", "provider.zhinao": "360智脑", "provider.zhipu": "智谱AI", "service.start": "启动服务", "service.startFailed": "启动失败", "service.starting": "启动中", "setting.altDoubleClick": "Alt双击", "setting.altSingleClick": "Alt单击", "setting.apiKey": "API密钥", "setting.apiUrl": "API地址", "setting.askEveryTime": "每次询问", "setting.autoLaunch": "开机启动", "setting.autoStart": "自启动", "setting.autoUpdate": "自动检测更新", "setting.basic": "基础设置", "setting.commandDoubleClick": "Command双击", "setting.commandSingleClick": "Command单击", "setting.controlDoubleClick": "Control双击", "setting.controlSingleClick": "Control单击", "setting.ctrlDoubleClick": "Ctrl双击", "setting.ctrlSingleClick": "Ctrl单击", "setting.cudaAcceleration": "CUDA加速", "setting.dataConfig": "数据配置", "setting.detachWindowHotkey": "分离窗口快捷键", "setting.env": "环境设置", "setting.exitDirectly": "直接退出", "setting.fixCommand": "修复命令", "setting.followSystem": "跟随系统", "setting.interfaceType": "接口类型", "setting.llm": "大模型设置", "setting.localModelDir": "本地模型目录", "setting.onClose": "点击关闭时", "setting.optionDoubleClick": "Option双击", "setting.optionSingleClick": "Option单击", "setting.pathChangeConfirm": "确认修改存储路径为 {path} ?", "setting.pathChangeRestart": "修改存储路径需要重启软件", "setting.storagePath": "文件存储路径", "setting.themeStyle": "主题样式", "setting.triggerType": "触发类型", "setting.wpm": "每分钟字数", "setup.allCompleted": "已完成所有设置", "setup.congratulations": "恭喜完成 {title} 设置", "setup.openSettings": "打开设置", "setup.verifyComplete": "验证完成", "soundAsr.copyResult": "复制识别结果", "soundAsrEdit.inputSearchContent": "请输入查找内容", "soundAsrEdit.invalidTimeRange": "时间范围不合法,必须在记录中间", "soundAsrEdit.mergedBlankSegments": "已合并连续空白片段", "soundAsrEdit.mergeOnlyContinuous": "只能合并连续的记录", "soundAsrEdit.noEditRecord": "没有编辑记录", "soundAsrEdit.noMatchFound": "未找到匹配的内容", "soundAsrEdit.optimizeComplete": "优化完成,成功修复 {successCount} 句,失败 {failCount} 句", "soundAsrEdit.replacedRecords": "已替换 {count} 条记录", "soundAsrEdit.time": "时间", "soundReplace.confirmComplete": "确认无误完成", "soundReplace.confirmText": "确认文字", "soundReplace.confirmTextDesc": "检查并确认识别出的文本内容", "soundReplace.extractAndRecognize": "提取音频并识别", "soundReplace.extractAndRecognizeDesc": "选择包含需要替换声音的视频文件", "soundReplace.extractAudio": "提取音频", "soundReplace.manualConfirmText": "手动确认文字", "soundReplace.modifyText": "修改文字", "soundReplace.reorderConfirm": "重排确认", "soundReplace.reverifyText": "重新校验文字", "soundReplace.saveAndSynthesize": "保存并合成", "soundReplace.submitTask": "提交任务", "soundReplace.synthesizeReplace": "声音合成替换", "soundReplace.synthesizeReplaceDesc": "设置声音合成模型参数,生成新的语音", "soundReplace.taskSubmitted": "任务已提交", "soundReplace.videoSynthesis": "视频合成", "status.cancelling": "正在取消任务", "status.deleting": "正在删除", "status.downloading": "正在下载", "status.downloadingProgress": "正在下载 {index}/{total}", "status.loading": "正在加载", "status.manuallyCompleted": "已手动完成", "status.notRunning": "暂未运行", "status.queuing": "排队中", "status.resuming": "正在继续", "status.retrying": "正在重试", "status.running": "运行中", "status.startedTime": "已启动 {time}", "status.stopped": "已停止", "status.submitting": "正在提交", "status.unprocessed": "未处理", "status.waiting": "等待中", "subtitleTts.audioSynthesis": "音频合成", "subtitleTts.parseSubtitle": "解析字幕", "subtitleTts.settings": "字幕转音频设置", "subtitleTts.synthesizedAudio": "合成音频", "store.searchPlaceholder": "输入关键词搜索", "system.actionManagement": "动作管理", "system.addFileLaunch": "增加一个文件启动", "system.aiModel": "AI模型", "system.dataCenter": "数据中心", "system.fileLaunch": "文件启动", "system.functionSettings": "功能设置", "system.hotkeys": "快捷键", "system.myAccount": "我的账号", "system.personalCenter": "个人中心", "system.pluginManagement": "插件管理", "system.preferences": "偏好设置", "task.batchTextSynthesis": "批量文本合成", "task.cancel": "取消任务", "task.cancelled": "任务已取消", "task.cloneSubmitted": "任务已经提交成功,等待克隆完成", "task.details": "任务详情", "task.editResult": "编辑识别结果", "task.longTextToAudio": "长文本转音频", "task.oneClickRun": "一键运行", "task.optimizeTimeline": "一键优化时间线", "task.processing": "处理中", "task.recognitionSubmitted": "语音识别任务已提交", "task.resume": "继续任务", "task.retry": "重试任务", "task.startRecognition": "开始识别", "task.startSynthesis": "开始合成", "task.startVideoGen": "开始生成视频", "task.submitSynthesis": "提交合成", "task.subtitleToAudio": "字幕转音频", "task.synthesisType": "合成类型", "task.synthesize": "合成", "task.videoGenSubmitted": "任务已经提交成功,等待视频生成完成", "task.view": "查看任务", "theme.dark": "暗黑", "theme.light": "明亮", "time.hour": "小时", "time.minute": "分钟", "time.second": "秒", "update.alreadyLatest": "已经是最新版本", "update.check": "检测更新", "update.checkFailed": "检测更新失败", "update.newVersionFound": "发现新版本{version},是否立即下载更新?", "user.comment": "用户评论", "user.energy": "能量", "user.enter": "用户进入", "user.knowledge": "用户知识", "user.like": "用户点赞", "user.name": "用户名", "user.reward": "用户打赏", "video.contentVideo": "内容视频", "video.loopBroadcast": "循环口播", "video.loopContent": "循环内容视频", "video.providerName": "供应商名称", "voice.add": "添加音色", "voice.clone": "声音克隆", "voice.cloneModel": "声音克隆模型", "voice.config": "声音配置", "voice.crossLanguage": "跨语种", "voice.currentConfig": "当前声音合成配置", "voice.file": "声音文件", "voice.recognition": "语音识别", "voice.recognitionConfig": "语音识别配置", "voice.recognitionModel": "语音识别模型", "voice.record": "录制音频", "voice.refAudioGuide1": "参考声音控制在 6~20s,保证声音清晰可见", "voice.refAudioGuide2": "参考声音需要大于 6s 小于 20s,保证声音清晰可见", "voice.refTextRequired": "需要输入参考声音的完整文字内容,部分模型需要使用", "voice.referenceAudio": "参考声音", "voice.referenceText": "参考文字", "voice.replace": "声音替换", "voice.replaceConfig": "声音替换设置", "voice.rerecord": "重新录制", "voice.select": "选择声音", "voice.selectFile": "选择声音文件", "voice.selectTimbre": "选择音色", "voice.synthesis": "语音合成", "voice.synthesisConfig": "声音合成配置", "voice.synthesisModel": "声音合成模型", "voice.timbre": "声音音色", "voice.timbreDesc": "音色说明", "voice.timbreManage": "音色管理", "voice.voice": "声音", "welcome.title": "欢迎使用 AIGCPanel !", "workflow.create": "创建工作流", "workflow.workflow": "工作流", "workflow.configureRecognitionAndGeneration": "请配置声音识别和声音生成服务", "workflow.inputVideoParam": "请输入视频参数", "workflow.paramErrorMissing": "参数错误:缺少 {items}", "workflow.soundGenerationService": "声音生成服务", "workflow.soundRecognitionService": "声音识别服务", "about.devModeSettings": "开发模式设置", "about.fastPanelHideOnBlur": "快速面板失焦隐藏", "action.backendCode": "后端代码", "action.code": "代码", "action.command": "命令", "action.smartArea": "智能区域", "action.webpage": "网页", "backup.connectFailed": "连接失败", "backup.fileFormat": "文件格式", "backup.notConfigured": "未配置WebDav服务,点击配置", "backup.password": "密码", "backup.placeholderSupport": "占位符支持", "backup.rootDir": "根目录", "backup.selectFile": "选择需要恢复的文件", "backup.selectRestoreFile": "请选择需要恢复的文件", "backup.startBackup": "开始备份", "backup.startRestore": "开始恢复", "backup.uploadToCloud": "上传到云端", "backup.restoreFromCloud": "从云端恢复", "backup.username": "用户名", "backup.webdavConfig": "配置", "backup.webdavSettings": "WebDav配置", "data.backupRestore": "备份/恢复", "data.clear": "清空", "data.clearConfirm": "确定要清空吗?", "data.clearSuccess": "清空成功", "data.deleteConfirm": "确定要删除吗?", "data.deleteSuccess": "删除成功", "data.docCount": "份文档", "data.filterPlaceholder": "输入关键词过滤", "data.title": "数据中心", "launch.actionName": "动作名称,如 截图", "launch.addHotkey": "增加一个快捷键", "launch.custom": "自定义", "launch.enterActionName": "请输入动作名称", "launch.hotkey": "快捷键", "log.noLogs": "暂无日志文件", "log.openFile": "打开文件", "main.clearAllConfirm": "确认清除全部?", "main.expandAll": "展开全部", "main.loading": "正在加载", "main.matchResults": "匹配结果", "main.multipleFiles": "多个文件", "main.multipleFolders": "多个文件夹", "main.multipleImages": "多个图片", "main.pinned": "已固定", "main.placeholder": "FocusAny,让您的工作专注高效", "main.recentlyUsed": "最近使用", "main.runError": "运行出现错误", "main.searchResults": "搜索结果", "main.starting": "正在启动", "main.window": "窗口", "plugin.detachWindow": "独立窗口显示", "plugin.actionNotFound": "未找到相关操作,请检查关键词或操作名称是否正确", "plugin.colorCopied": "颜色 {color} 已复制到剪贴板", "plugin.colorCopyShortcut": "复制 {shortcut}", "plugin.errorLog": "插件{name}错误 : {error}", "plugin.exitEsc": "退出 ESC", "plugin.installComplete": "插件 {title} 安装完成", "plugin.installing": "正在安装插件", "plugin.newWindow": "新窗口", "plugin.noPermission": "插件没有权限({permission})", "plugin.opening": "正在打开插件", "plugin.notExist": "插件 {name} 不存在", "plugin.screenshotHint": "请使用截图工具截图", "plugin.selectSavePath": "选择保存路径", "editor.noPluginForFile": "没有找到可以打开文件的插件", "file.notFoundOrReadFailed": "文件不存在或读取失败", "file.unsupportedType": "不支持的文件类型", "screenshot.edit": "截图编辑", "system.title": "系统设置", "system.desc": "提供基础系统功能", "system.apps": "应用软件", "system.appsDesc": "提供系统应用软件的搜索和打开", "system.appsIndexed": "应用软件索引完成", "system.appsIndexing": "正在分析应用软件,稍后才可以搜索到应用软件哦~", "system.fileLaunchDesc": "提供文件一键启动功能", "system.workflowDesc": "提供工作流管理功能", "system.storeDesc": "提供插件应用市场管理功能", "system.about": "关于我们", "system.screenshot": "截图", "system.colorPicker": "颜色拾取", "system.screenRecord": "屏幕录制", "system.lockScreen": "锁屏", "system.lanIP": "局域网IP", "system.ipCopied": "IP地址 {ip} 已复制到剪贴板", "tray.visitWebsite": "访问官网", "debug.info": "调试信息", "debug.copyRoute": "复制路由", "setting.darkTheme": "暗黑", "setting.fastPanel": "快捷面板", "setting.fastPanelHotkey": "快捷面板呼出快捷键", "setting.functionSettings": "功能设置", "setting.invokeHotkey": "呼出快捷键", "setting.language": "界面语言", "setting.lightTheme": "明亮" } ================================================ FILE: src/layouts/Main.vue ================================================ ================================================ FILE: src/layouts/Raw.vue ================================================ ================================================ FILE: src/lib/api.ts ================================================ import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"; import { merge } from "lodash-es"; import { Dialog } from "./dialog"; import { AppConfig } from "../config"; import { user } from "../store/modules/user"; function createService() { const service = axios.create(); service.interceptors.request.use( (config) => config, (error) => Promise.reject(error), ); service.interceptors.response.use( (response) => { const apiData = response.data; const responseType = response.request?.responseType; if (responseType === "blob" || responseType === "arraybuffer") return apiData; const code = apiData.code; // if (code === undefined) { // ElMessage.error("非本系统的接口") // return Promise.reject(new Error("非本系统的接口")) // } // switch (code) { // case 0: // // 本系统采用 code === 0 来表示没有业务错误 // return apiData // case 401: // // Token 过期时 // return logout() // default: // // 不是正确的 code // ElMessage.error(apiData.message || "Error") // return Promise.reject(new Error("Error")) // } return apiData; }, (error) => { return Promise.reject(error); }, ); return service; } function createRequest(service: AxiosInstance) { return function (config: AxiosRequestConfig): Promise { const defaultConfig = { headers: { "User-Agent": window.$mapi.app.getUserAgent(), "Api-Token": user.apiToken ? user.apiToken : undefined, "Content-Type": "application/json", }, timeout: 60 * 1000, baseURL: AppConfig.apiBaseUrl, data: {}, }; // 将默认配置 defaultConfig 和传入的自定义配置 config 进行合并成为 mergeConfig const mergeConfig = merge(defaultConfig, config); return service(mergeConfig).then((response) => response as T); }; } const service = createService(); export const request = createRequest(service); export const defaultResponseProcessor = ( res: ApiResult, success: Function | null = null, error: Function | null = null, ) => { if (res.code) { if (error) { if (!error(res)) { Dialog.tipError(res.msg); } } else { Dialog.tipError(res.msg); } } else { if (success) { if (success(res)) { if (res.msg) { Dialog.tipSuccess(res.msg); } } } else { if (res.msg) { Dialog.tipSuccess(res.msg); } } } }; ================================================ FILE: src/lib/audio.ts ================================================ export const AudioUtil = { audioBufferEmpty() { const emptyLength = 1024 * 100; const buffer = new AudioBuffer({ length: emptyLength, numberOfChannels: 2, sampleRate: 8000, }); for (let channel = 0; channel < 2; channel++) { const data = buffer.getChannelData(channel); for (let i = 0; i < emptyLength; i++) { data[i] = 0; } } return buffer; }, audioBufferCut(buffer: AudioBuffer, start: number, end: number) { const numChannels = buffer.numberOfChannels; const sampleRate = buffer.sampleRate; const length = buffer.length; const startOffset = Math.floor(start * sampleRate); const endOffset = Math.floor(end * sampleRate); const targetLength = endOffset - startOffset; const targetBuffer = new AudioBuffer({ length: targetLength, numberOfChannels: numChannels, sampleRate: sampleRate, }); for (let channel = 0; channel < numChannels; channel++) { const sourceChannel = buffer.getChannelData(channel); const targetChannel = targetBuffer.getChannelData(channel); for (let i = 0; i < targetLength; i++) { targetChannel[i] = sourceChannel[startOffset + i]; } } return targetBuffer; }, audioBufferConvert( buffer: AudioBuffer, targetSampleRate: number, targetChannelNum: number, ) { targetChannelNum = targetChannelNum || buffer.numberOfChannels; const numChannels = buffer.numberOfChannels; const sampleRate = buffer.sampleRate; const length = buffer.length; const targetLength = Math.floor( (length * targetSampleRate) / sampleRate, ); const targetBuffer = new AudioBuffer({ length: targetLength, numberOfChannels: targetChannelNum, sampleRate: targetSampleRate, }); for (let channel = 0; channel < targetChannelNum; channel++) { const sourceChannel = buffer.getChannelData(channel % numChannels); const targetChannel = targetBuffer.getChannelData(channel); for (let i = 0; i < targetLength; i++) { const sourceIndex = Math.floor( (i * sampleRate) / targetSampleRate, ); targetChannel[i] = sourceChannel[sourceIndex]; } } return targetBuffer; }, audioBufferToWav(buffer: AudioBuffer) { const numChannels = buffer.numberOfChannels; const sampleRate = buffer.sampleRate; const format = 1; const bitDepth = 16; const bytesPerSample = bitDepth / 8; const blockAlign = numChannels * bytesPerSample; const dataSize = buffer.length * blockAlign; const view = new DataView(new ArrayBuffer(44 + dataSize)); view.setUint32(0, 1380533830, false); view.setUint32(4, 44 + dataSize - 8, true); view.setUint32(8, 1463899717, false); view.setUint32(12, 1718449184, false); view.setUint32(16, 16, true); view.setUint16(20, format, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * blockAlign, true); view.setUint16(32, blockAlign, true); view.setUint16(34, bitDepth, true); view.setUint32(36, 1635017060, true); view.setUint32(40, dataSize, true); let offset = 44; for (let i = 0; i < buffer.length; i++) { for (let channel = 0; channel < numChannels; channel++) { const sample = buffer.getChannelData(channel)[i]; const intSample = Math.max(-1, Math.min(1, sample)); view.setInt16( offset, Math.round( intSample < 0 ? intSample * 0x8000 : intSample * 0x7fff, ), true, ); offset += 2; } } return new Uint8Array(view.buffer); }, audioBufferDuration(buffer: AudioBuffer) { return buffer.duration; }, audioBufferToWavBlob(buffer: AudioBuffer) { return new Blob([this.audioBufferToWav(buffer)], { type: "audio/wav" }); }, fileToAudioBuffer(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const arrayBuffer = reader.result as ArrayBuffer; const context = new AudioContext(); context.decodeAudioData(arrayBuffer, resolve, reject); }; reader.readAsArrayBuffer(file); }); }, parseAudioFile(file: File) { return new Promise<{ duration: number; sampleRate: number; numberOfChannels: number; }>((resolve, reject) => { this.fileToAudioBuffer(file) .then((buffer) => { resolve({ duration: buffer.duration, sampleRate: buffer.sampleRate, numberOfChannels: buffer.numberOfChannels, }); }) .catch(reject); }); }, }; ================================================ FILE: src/lib/components/Prompt.vue ================================================ ================================================ FILE: src/lib/dialog.ts ================================================ import { Message, MessageReturn, Modal } from "@arco-design/web-vue"; import Prompt from "./components/Prompt.vue"; import { h } from "vue"; import { i18n, t } from "../lang"; let loadingLayers: MessageReturn[] = []; export const Dialog = { tipSuccess: (msg: string) => { Message.success(msg); }, tipError: (msg: string) => { Message.error(msg); }, confirm: (content: string, title: string | null = null): Promise => { title = title || t("common.tip"); return new Promise((resolve, reject) => { Modal.confirm({ title, content, titleAlign: "start", simple: false, width: "25rem", modalClass: "arco-modal-confirm", okText: t("common.confirm"), cancelText: t("common.cancel"), onOk: () => { resolve(); }, onCancel: () => { // reject(); }, }); }); }, alertSuccess: ( content: string, title: string | null = null, ): Promise => { title = title || t("common.tip"); return new Promise((resolve) => { Modal.confirm({ title, content, simple: false, width: "25rem", onOk: () => { resolve(); }, }); }); }, alertError: ( content: string, title: string | null = null, ): Promise => { title = title || t("common.tip"); return new Promise((resolve) => { Modal.confirm({ title, content, simple: false, width: "25rem", onOk: () => { resolve(); }, }); }); }, loadingOn: (content: string | null = null) => { content = content || t("common.loadingDots"); const loading = Message.loading({ content, duration: 0, }); loadingLayers.push(loading); }, loadingUpdate: (content: string) => { if (loadingLayers.length > 0) { const contentContainer = document.querySelector( ".arco-message-list .arco-message-loading .arco-message-content", ); if (contentContainer) { contentContainer.innerHTML = content; } } }, loadingOff: () => { const loading = loadingLayers.pop(); if (loading) { loading.close(); } }, prompt: ( content: string, defaultValue: string = "", ): Promise => { return new Promise((resolve) => { let inputValue = defaultValue; Modal.open({ title: content, simple: false, titleAlign: "start", content: () => { return h(Prompt, { value: defaultValue, onChange: (value: string) => { inputValue = value; }, }); }, width: "25rem", onOk: () => { resolve(inputValue); }, }); }); }, }; ================================================ FILE: src/lib/env.ts ================================================ export const isDev = process.env.NODE_ENV === "development"; ================================================ FILE: src/lib/error.ts ================================================ import { t } from "../lang"; export function mapError(msg: any) { if (typeof msg !== "string") { msg = msg.toString(); } const map = { PublishVersionNotMatch: "error.publishVersionNotMatch", PluginNotExists: "error.pluginNotExists", PluginFormatError: "error.pluginFormatError", PluginAlreadyExists: "error.pluginAlreadyExists", PluginNotSupportPlatform: "error.pluginNotSupportPlatform", PluginVersionNotMatch: "error.pluginVersionNotMatch", PluginEditionNotMatch: "error.pluginEditionNotMatch", PluginReleaseDocNotFound: "error.pluginReleaseDocNotFound", PluginReleaseDocFormatError: "error.pluginReleaseDocFormatError", }; for (let key in map) { if (msg.includes(key)) { const translationKey = map[key]; // regex PluginReleaseDocFormatError:-11 const regex = new RegExp(`${key}:(-?\\d+):?([\\w\\d]*)`); const match = msg.match(regex); // console.log('match', match) let error = t(translationKey); if (match) { error += `(${match[1]})`; if (match[2]) { error += `(${match[2]})`; } } return error; } } return msg; } ================================================ FILE: src/lib/event.ts ================================================ import { TinyEmitter } from "tiny-emitter"; const emitter = new TinyEmitter(); export const GlobalEvent = { on: function (event: string, callback: Function) { emitter.on(event, callback); }, once: function (event: string, callback: Function) { emitter.once(event, callback); }, off: function (event: string, callback: Function) { emitter.off(event, callback); }, emit: function (event: string, ...args: any[]) { emitter.emit(event, ...args); }, }; ================================================ FILE: src/lib/file.ts ================================================ import SparkMD5 from "spark-md5"; export const FileUtil = { extensionToType(extension: string) { const mime = { mp3: "audio/mpeg", wav: "audio/wav", mp4: "video/mp4", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", svg: "image/svg+xml", }; return mime[extension] || ""; }, bufferToBlob(buffer: ArrayBuffer, type: string) { if (!type.indexOf("/")) { type = this.extensionToType(type); } return new Blob([buffer], { type: type }); }, base64ToBuffer(base64: string) { const binaryString = window.atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; }, blobToFile(blob: Blob, name: string) { return new File([blob], name); }, urlToBlob(url: string): Promise { return fetch(url).then((res) => res.blob()); }, blobToBase64Url(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { resolve(reader.result as string); }; reader.onerror = (e) => { reject(e); }; reader.readAsDataURL(blob); }); }, getExt(path: string) { const ext = path.lastIndexOf("."); if (ext >= 0) { return path.substring(ext + 1).toLowerCase(); } return ""; }, getBaseName(path: string, withExt: boolean = false) { // windows if (path.includes("\\")) { path = path.replace(/\\/g, "/"); } const last = path.lastIndexOf("/"); if (last >= 0) { path = path.substring(last + 1); } if (!withExt) { const ext = path.lastIndexOf("."); if (ext >= 0) { path = path.substring(0, ext); } return path; } return path; }, async md5File(file: File): Promise { return new Promise((resolve, reject) => { if (!SparkMD5) { reject(new Error("SparkMD5 not found")); return; } const chunkSize = 2097152; // Read in chunks of 2MB const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onload = (e: any) => { if (e.target.error) { reject(e.target.error); return; } spark.append(e.target.result); // Append array buffer currentChunk++; if (currentChunk < chunks) { loadNext(); } else { const md5 = spark.end(); resolve(md5); } }; fileReader.onerror = () => { reject(fileReader.error); }; function loadNext() { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(file.slice(start, end)); } loadNext(); }); }, async md5Stream(stream: ReadableStream): Promise { if (!SparkMD5) { throw new Error("SparkMD5 not found"); } const reader = stream.getReader(); const spark: any = new SparkMD5.ArrayBuffer(); return new Promise((resolve, reject) => { function processChunk() { reader .read() .then(({ done, value }) => { if (done) { const md5 = spark.end(); resolve(md5); return; } if (value) { spark.append(value.buffer); } processChunk(); }) .catch((err) => { reject(err); }); } processChunk(); }); }, formatSize: (bytes: number) => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }, }; ================================================ FILE: src/lib/markdown.ts ================================================ import Showdown from "showdown"; const converter = new Showdown.Converter(); export const MarkdownUtil = { toHtml(markdown: string): string { return converter.makeHtml(markdown); }, }; ================================================ FILE: src/lib/storage.ts ================================================ export const StorageUtil = { /** * @Util 删除 * @param key String 键 */ remove: function (key: string): void { window.localStorage.removeItem(key); }, /** * @Util 存储数据 * @param key String 键 * @param value String|Object|Array 值 */ set: function (key: string, value: any): void { window.localStorage.setItem(key, JSON.stringify(value)); }, /** * @Util 获取数据 * @param key String 键 * @param defaultValue String|Object|Array 默认值 * @return String|Object|Array 返回值 */ get: function (key: string, defaultValue: any): any { let value = window.localStorage.getItem(key); if (null === value) { return defaultValue; } try { return JSON.parse(value); } catch (e) {} return defaultValue; }, /** * @Util 获取数组数据 * @param key String 键 * @param defaultValue Array 默认值 * @return Array 返回值 */ getArray: function (key: string, defaultValue?: any): any { defaultValue = defaultValue || []; let value = window.localStorage.getItem(key); if (!value) { return defaultValue; } try { value = JSON.parse(value); if (!Array.isArray(value)) { return defaultValue; } return value; } catch (e) {} return defaultValue; }, /** * @Util 获取对象数据 * @param key String 键 * @param defaultValue Object 默认值 * @return Array 返回值 */ getObject: function (key: string, defaultValue?: any): any { defaultValue = defaultValue || {}; let value = window.localStorage.getItem(key); if (!value) { return defaultValue; } try { value = JSON.parse(value); if (null === value) { return defaultValue; } if (!Array.isArray(value) && typeof value === "object") { return value; } return defaultValue; } catch (e) {} return defaultValue; }, }; ================================================ FILE: src/lib/toggle.ts ================================================ import { computed, ref, type Ref, type ComputedRef } from "vue"; export const ToggleUtil = { cachePool: new Map }>(), gc() { const now = Date.now(); for (const [key, { expire }] of this.cachePool) { if (expire < now) { this.cachePool.delete(key); } } }, get(biz: string, bizId: any, defaultValue: boolean = false) { const key = `Toggle:${biz}:${bizId}`; if (!this.cachePool.has(key)) { const refValue = ref(defaultValue); this.cachePool.set(key, { expire: Date.now() + 3600 * 1000, value: refValue, }); return refValue; } const cached = this.cachePool.get(key)!; cached.expire = Date.now() + 3600 * 1000; ToggleUtil.gc(); return cached.value; }, toggle(biz: string, bizId: any) { const refValue = this.get(biz, bizId); refValue.value = !refValue.value; return refValue.value; }, }; ================================================ FILE: src/lib/ui.ts ================================================ type DomListener = { dom: HTMLElement; callback: (width: number, height: number) => void; }; let domListeners: DomListener[] = []; const resizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { domListeners.forEach((item) => { if (item.dom === entry.target) { const { width, height } = entry.contentRect; item.callback(width, height); } }); }); }); type WindowListener = { callback: (width: number, height: number) => void; }; let windowListeners: WindowListener[] = []; window.addEventListener("resize", () => { windowListeners.forEach((item) => { item.callback(window.innerWidth, window.innerHeight); }); }); export const UI = { onWindowResize(callback: (width: number, height: number) => void) { windowListeners.push({ callback }); }, offWindowResize(callback: (width: number, height: number) => void) { windowListeners = windowListeners.filter( (item) => item.callback !== callback, ); }, onResize( dom: HTMLElement | null, callback: (width: number, height: number) => void, ) { if (!dom) return; domListeners.push({ dom, callback }); resizeObserver.observe(dom); }, offResize(dom: HTMLElement | null) { if (!dom) return; domListeners = domListeners.filter((item) => item.dom !== dom); resizeObserver.unobserve(dom); }, fireResize(dom: HTMLElement) { domListeners.forEach((item) => { if (item.dom === dom) { const { width, height } = dom.getBoundingClientRect(); item.callback(width, height); } }); }, smoothScrollTop: (element: HTMLElement, to: number, duration = 200) => { return new Promise((resolve) => { const start = element.scrollTop; const change = to - start; const startTime = performance.now(); const animate = (now) => { const progress = Math.min((now - startTime) / duration, 1); const eased = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2; element.scrollTop = start + change * eased; if (progress < 1) { requestAnimationFrame(animate); } else { resolve(undefined); } }; requestAnimationFrame(animate); }); }, }; export class TabContentScroller { private option: { activeClass: string; }; private tabContainer: HTMLElement; private contentContainer: HTMLElement; private isScrolling = false; private scrollEndTimer: any | null = null; private scrollEndCallback: (() => void) | null = null; constructor( tabContainer: HTMLElement, contentContainer: HTMLElement, option: {} = {}, ) { this.option = Object.assign( { activeClass: "active", }, option, ) || {}; this.tabContainer = tabContainer; this.contentContainer = contentContainer; this.init(); } init() { this.tabContainer.addEventListener( "click", this.onTabClickEvent.bind(this), ); this.contentContainer.addEventListener( "scroll", this.onContentScrollEvent.bind(this), ); } destroy() { this.tabContainer.removeEventListener( "click", this.onTabClickEvent.bind(this), ); this.contentContainer.removeEventListener( "scroll", this.onContentScrollEvent.bind(this), ); } onTabClickEvent(e: MouseEvent) { const parentSection = (e.target as HTMLElement).closest( "[data-section]", ); const name = parentSection?.getAttribute("data-section"); if (name) { this.scrollTo(name); this.scrollEndCallback = () => { this.forceActiveTab(name); }; } } onContentScrollEvent(e: Event) { this.isScrolling = true; if (this.scrollEndTimer) { clearTimeout(this.scrollEndTimer); } this.scrollEndTimer = setTimeout(() => { this.isScrolling = false; this.scrollEndTimer = null; if (this.scrollEndCallback) { this.scrollEndCallback(); this.scrollEndCallback = null; } }, 100); const tabs = this.tabContainer.querySelectorAll("[data-section]"); for (let i = 0; i < tabs.length; i++) { const tab = tabs[i]; tab.classList.remove(this.option.activeClass); } const sections = this.contentContainer.querySelectorAll("[data-section]"); for (let i = 0; i < sections.length; i++) { const section = sections[i]; const rect = section.getBoundingClientRect(); if (rect.top < 100 && rect.bottom > 100) { const name = section.getAttribute("data-section") || ""; const tab = this.tabContainer.querySelector( `[data-section="${name}"]`, ); if (tab) { tab.classList.add(this.option.activeClass); } break; } } } forceActiveTab(name: string) { const tabs = this.tabContainer.querySelectorAll("[data-section]"); for (let i = 0; i < tabs.length; i++) { const tab = tabs[i]; const tabName = tab.getAttribute("data-section") || ""; if (tabName === name) { tab.classList.add(this.option.activeClass); } else { tab.classList.remove(this.option.activeClass); } } } scrollTo(name: string) { const tab = this.tabContainer.querySelector(`[data-section="${name}"]`); if (!tab) { return; } const content = this.contentContainer.querySelector( `[data-section="${name}"]`, ); if (!content) { return; } content.scrollIntoView({ behavior: "smooth", }); } } ================================================ FILE: src/lib/util.ts ================================================ import dayjs from "dayjs"; import { Base64 } from "js-base64"; import { t } from "../lang"; export const sleep = (time = 1000) => { return new Promise((resolve) => { setTimeout(() => resolve(true), time); }); }; export const wait = (callback: () => boolean, interval = 10) => { return new Promise((resolve) => { const timer = setInterval(() => { if (callback()) { clearInterval(timer); resolve(true); } }, interval); }); }; /** * 精确计时器 * @param callback * @param interval * @returns */ export function preciseInterval(callback: () => void, interval: number) { let expected = performance.now() + interval; let stop = false; function step(timestamp: number) { if (stop) return; if (timestamp >= expected) { callback(); // 累积期望的时间,以保持精确的间隔 expected += interval; } requestAnimationFrame(step); } requestAnimationFrame(step); // 返回一个对象包含取消方法 return { cancel: () => { stop = true; }, }; } export const StringUtil = { random(length: number = 16) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }, uuid: () => { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( /[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }, ); }, replaceParam: (str: string, param: any) => { return str.replace(/{(.*?)}/g, (match: string, key: string) => { return param[key] || match; }); }, }; export const TimeUtil = { timestamp() { return Math.floor(Date.now() / 1000); }, datetimeToTimestamp(datetime: string) { return dayjs(datetime).unix(); }, timestampMS() { return Date.now(); }, format(time: number, format: string = "YYYY-MM-DD HH:mm:ss") { return dayjs(time).format(format); }, formatDate(time: number) { return dayjs(time).format("YYYY-MM-DD"); }, dateString() { return dayjs().format("YYYYMMDD"); }, datetimeString() { return dayjs().format("YYYYMMDD_HHmmss"); }, secondsToTime(seconds: number, showMs: boolean = false) { const sec = Math.floor(seconds); const ms = Math.floor((seconds - sec) * 1000); let h: any = Math.floor(sec / 3600); let m: any = Math.floor((sec % 3600) / 60); let s: any = Math.floor(sec % 60); if (h < 10) h = "0" + h; if (m < 10) m = "0" + m; if (s < 10) s = "0" + s; const result = "00" == h ? `${m}:${s}` : `${h}:${m}:${s}`; if (showMs) { let f: any = ms; if (f < 10) f = "00" + f; else if (f < 100) f = "0" + f; return `${result}.${f}`; } return result; }, msToTime(ms: number) { return this.secondsToTime(ms / 1000, true); }, secondsToHuman(seconds: number) { seconds = parseInt(seconds.toString()); let h: any = Math.floor(seconds / 3600); let m: any = Math.floor((seconds % 3600) / 60); let s: any = Math.floor(seconds % 60); const result: string[] = []; if (h > 0) result.push(`${h}${t("time.hour")}`); if (m > 0) result.push(`${m}${t("time.minute")}`); if (s > 0) result.push(`${s}${t("time.second")}`); return result.join(""); }, replacePattern(text: string) { return text .replaceAll("{year}", dayjs().format("YYYY")) .replaceAll("{month}", dayjs().format("MM")) .replaceAll("{day}", dayjs().format("DD")) .replaceAll("{hour}", dayjs().format("HH")) .replaceAll("{minute}", dayjs().format("mm")) .replaceAll("{second}", dayjs().format("ss")); }, }; export const EncodeUtil = { base64Encode(str: string) { return Base64.encode(str); }, base64Decode(str: string) { return Base64.decode(str); }, }; export const VersionUtil = { /** * 检测版本是否匹配 * @param v string * @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0 */ match(v: string, match: string) { if (match === "*") { return true; } if (match.startsWith(">=") && this.ge(v, match.substring(2))) { return true; } if (match.startsWith(">") && this.gt(v, match.substring(1))) { return true; } if (match.startsWith("<=") && this.le(v, match.substring(2))) { return true; } if (match.startsWith("<") && this.lt(v, match.substring(1))) { return true; } return this.eq(v, match); }, compare(v1: string, v2: string) { const v1Arr = v1.split("."); const v2Arr = v2.split("."); for (let i = 0; i < v1Arr.length; i++) { const v1Num = parseInt(v1Arr[i]); const v2Num = parseInt(v2Arr[i]); if (v1Num > v2Num) { return 1; } else if (v1Num < v2Num) { return -1; } } return 0; }, gt(v1: string, v2: string) { return VersionUtil.compare(v1, v2) > 0; }, ge(v1: string, v2: string) { return VersionUtil.compare(v1, v2) >= 0; }, lt(v1: string, v2: string) { return VersionUtil.compare(v1, v2) < 0; }, le: (v1: string, v2: string) => { return VersionUtil.compare(v1, v2) <= 0; }, eq: (v1: string, v2: string) => { return VersionUtil.compare(v1, v2) === 0; }, }; export const BrowserUtil = { isMac() { return navigator.platform.toUpperCase().indexOf("MAC") >= 0; }, isWindows() { return navigator.platform.toUpperCase().indexOf("WIN") >= 0; }, isLinux() { return navigator.platform.toUpperCase().indexOf("LINUX") >= 0; }, }; export const ShellUtil = { quotaPath(p: string) { return `"${p}"`; }, }; export const ObjectUtil = { clone(obj: any) { return JSON.parse(JSON.stringify(obj)); }, }; export const DownloadUtil = { downloadFile(content: string, filename?: string) { const blob = new Blob([content], { type: "application/octet-stream" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename || `download_${TimeUtil.datetimeString()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, }; ================================================ FILE: src/main.ts ================================================ import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; import ArcoVue, { Message } from "@arco-design/web-vue"; import ArcoVueIcon from "@arco-design/web-vue/es/icon"; import "@arco-design/web-vue/dist/arco.css"; import { i18n, t } from "./lang"; import "./style.less"; import { Dialog } from "./lib/dialog"; import { TaskManager } from "./task"; import { useSettingStore } from "./store/modules/setting"; import { reportErrorRender } from "../electron/mapi/log/beacon-render"; const settingStore = useSettingStore(); const app = createApp(App); app.use(ArcoVue); app.use(ArcoVueIcon); app.use(i18n); app.use(store); app.use(router); Message._context = app._context; app.config.globalProperties.$mapi = window.$mapi; app.config.globalProperties.$dialog = Dialog; app.config.globalProperties.$t = t as any; TaskManager.init(); app.mount("#app").$nextTick(() => { postMessage({ payload: "removeLoading" }, "*"); window.addEventListener("error", (ev) => { reportErrorRender( ev.message, ev.error?.stack, ev.filename, ev.lineno, ev.colno, ); }); window.addEventListener("unhandledrejection", (ev) => { const err = ev.reason; const msg = err instanceof Error ? err.message : String(err); const stack = err instanceof Error ? err.stack : undefined; reportErrorRender(msg, stack); }); }); ================================================ FILE: src/module/Model/ModelGenerateButton.vue ================================================ ================================================ FILE: src/module/Model/ModelGenerator.vue ================================================ ================================================ FILE: src/module/Model/ModelPromptDataConfigButton.vue ================================================ ================================================ FILE: src/module/Model/ModelSelector.vue ================================================ ================================================ FILE: src/module/Model/ModelSetting.vue ================================================ ================================================ FILE: src/module/Model/ModelSettingDialog.vue ================================================ ================================================ FILE: src/module/Model/components/ModelAddDialog.vue ================================================ ================================================ FILE: src/module/Model/components/ModelEditDialog.vue ================================================ ================================================ FILE: src/module/Model/components/ProviderAddDialog.vue ================================================ ================================================ FILE: src/module/Model/components/ProviderEditDialog.vue ================================================ ================================================ FILE: src/module/Model/components/ProviderTestDialog.vue ================================================ ================================================ FILE: src/module/Model/models.ts ================================================ import Ai360ModelLogo from "./assets/image/models/360.png"; import Ai360ModelLogoDark from "./assets/image/models/360_dark.png"; import AdeptModelLogo from "./assets/image/models/adept.png"; import AdeptModelLogoDark from "./assets/image/models/adept_dark.png"; import Ai21ModelLogo from "./assets/image/models/ai21.png"; import Ai21ModelLogoDark from "./assets/image/models/ai21_dark.png"; import AimassModelLogo from "./assets/image/models/aimass.png"; import AimassModelLogoDark from "./assets/image/models/aimass_dark.png"; import AisingaporeModelLogo from "./assets/image/models/aisingapore.png"; import AisingaporeModelLogoDark from "./assets/image/models/aisingapore_dark.png"; import BaichuanModelLogo from "./assets/image/models/baichuan.png"; import BaichuanModelLogoDark from "./assets/image/models/baichuan_dark.png"; import BgeModelLogo from "./assets/image/models/bge.webp"; import BigcodeModelLogo from "./assets/image/models/bigcode.webp"; import BigcodeModelLogoDark from "./assets/image/models/bigcode_dark.webp"; import ChatGLMModelLogo from "./assets/image/models/chatglm.png"; import ChatGLMModelLogoDark from "./assets/image/models/chatglm_dark.png"; import ChatGptModelLogo from "./assets/image/models/chatgpt.jpeg"; import ClaudeModelLogo from "./assets/image/models/claude.png"; import ClaudeModelLogoDark from "./assets/image/models/claude_dark.png"; import CodegeexModelLogo from "./assets/image/models/codegeex.png"; import CodegeexModelLogoDark from "./assets/image/models/codegeex_dark.png"; import CodestralModelLogo from "./assets/image/models/codestral.png"; import CohereModelLogo from "./assets/image/models/cohere.png"; import CohereModelLogoDark from "./assets/image/models/cohere_dark.png"; import CopilotModelLogo from "./assets/image/models/copilot.png"; import CopilotModelLogoDark from "./assets/image/models/copilot_dark.png"; import DalleModelLogo from "./assets/image/models/dalle.png"; import DalleModelLogoDark from "./assets/image/models/dalle_dark.png"; import DbrxModelLogo from "./assets/image/models/dbrx.png"; import DeepSeekModelLogo from "./assets/image/models/deepseek.png"; import DeepSeekModelLogoDark from "./assets/image/models/deepseek_dark.png"; import DianxinModelLogo from "./assets/image/models/dianxin.png"; import DianxinModelLogoDark from "./assets/image/models/dianxin_dark.png"; import DoubaoModelLogo from "./assets/image/models/doubao.png"; import DoubaoModelLogoDark from "./assets/image/models/doubao_dark.png"; import { default as EmbeddingModelLogo, default as EmbeddingModelLogoDark, } from "./assets/image/models/embedding.png"; import FlashaudioModelLogo from "./assets/image/models/flashaudio.png"; import FlashaudioModelLogoDark from "./assets/image/models/flashaudio_dark.png"; import FluxModelLogo from "./assets/image/models/flux.png"; import FluxModelLogoDark from "./assets/image/models/flux_dark.png"; import GeminiModelLogo from "./assets/image/models/gemini.png"; import GeminiModelLogoDark from "./assets/image/models/gemini_dark.png"; import GemmaModelLogo from "./assets/image/models/gemma.png"; import GemmaModelLogoDark from "./assets/image/models/gemma_dark.png"; import { default as GoogleModelLogo, default as GoogleModelLogoDark, } from "./assets/image/models/google.png"; import ChatGPT35ModelLogo from "./assets/image/models/gpt_3.5.png"; import ChatGPT4ModelLogo from "./assets/image/models/gpt_4.png"; import { default as ChatGPT35ModelLogoDark, default as ChatGPT4ModelLogoDark, default as ChatGptModelLogoDark, default as ChatGPTo1ModelLogoDark, } from "./assets/image/models/gpt_dark.png"; import ChatGPTo1ModelLogo from "./assets/image/models/gpt_o1.png"; import GrokModelLogo from "./assets/image/models/grok.png"; import GrokModelLogoDark from "./assets/image/models/grok_dark.png"; import GrypheModelLogo from "./assets/image/models/gryphe.png"; import GrypheModelLogoDark from "./assets/image/models/gryphe_dark.png"; import HailuoModelLogo from "./assets/image/models/hailuo.png"; import HailuoModelLogoDark from "./assets/image/models/hailuo_dark.png"; import HuggingfaceModelLogo from "./assets/image/models/huggingface.png"; import HuggingfaceModelLogoDark from "./assets/image/models/huggingface_dark.png"; import HunyuanModelLogo from "./assets/image/models/hunyuan.png"; import HunyuanModelLogoDark from "./assets/image/models/hunyuan_dark.png"; import IbmModelLogo from "./assets/image/models/ibm.png"; import IbmModelLogoDark from "./assets/image/models/ibm_dark.png"; import InternlmModelLogo from "./assets/image/models/internlm.png"; import InternlmModelLogoDark from "./assets/image/models/internlm_dark.png"; import InternvlModelLogo from "./assets/image/models/internvl.png"; import JinaModelLogo from "./assets/image/models/jina.png"; import JinaModelLogoDark from "./assets/image/models/jina_dark.png"; import KeLingModelLogo from "./assets/image/models/keling.png"; import KeLingModelLogoDark from "./assets/image/models/keling_dark.png"; import LlamaModelLogo from "./assets/image/models/llama.png"; import LlamaModelLogoDark from "./assets/image/models/llama_dark.png"; import LLavaModelLogo from "./assets/image/models/llava.png"; import LLavaModelLogoDark from "./assets/image/models/llava_dark.png"; import LumaModelLogo from "./assets/image/models/luma.png"; import LumaModelLogoDark from "./assets/image/models/luma_dark.png"; import MagicModelLogo from "./assets/image/models/magic.png"; import MagicModelLogoDark from "./assets/image/models/magic_dark.png"; import MediatekModelLogo from "./assets/image/models/mediatek.png"; import MediatekModelLogoDark from "./assets/image/models/mediatek_dark.png"; import MicrosoftModelLogo from "./assets/image/models/microsoft.png"; import MicrosoftModelLogoDark from "./assets/image/models/microsoft_dark.png"; import MidjourneyModelLogo from "./assets/image/models/midjourney.png"; import MidjourneyModelLogoDark from "./assets/image/models/midjourney_dark.png"; import { default as MinicpmModelLogo, default as MinicpmModelLogoDark, } from "./assets/image/models/minicpm.webp"; import MinimaxModelLogo from "./assets/image/models/minimax.png"; import MinimaxModelLogoDark from "./assets/image/models/minimax_dark.png"; import MistralModelLogo from "./assets/image/models/mixtral.png"; import MistralModelLogoDark from "./assets/image/models/mixtral_dark.png"; import MoonshotModelLogo from "./assets/image/models/moonshot.png"; import MoonshotModelLogoDark from "./assets/image/models/moonshot_dark.png"; import { default as NousResearchModelLogo, default as NousResearchModelLogoDark, } from "./assets/image/models/nousresearch.png"; import NvidiaModelLogo from "./assets/image/models/nvidia.png"; import NvidiaModelLogoDark from "./assets/image/models/nvidia_dark.png"; import PalmModelLogo from "./assets/image/models/palm.png"; import PalmModelLogoDark from "./assets/image/models/palm_dark.png"; import { default as PerplexityModelLogo, default as PerplexityModelLogoDark, } from "./assets/image/models/perplexity.png"; import PixtralModelLogo from "./assets/image/models/pixtral.png"; import PixtralModelLogoDark from "./assets/image/models/pixtral_dark.png"; import QwenModelLogo from "./assets/image/models/qwen.png"; import QwenModelLogoDark from "./assets/image/models/qwen_dark.png"; import RakutenaiModelLogo from "./assets/image/models/rakutenai.png"; import RakutenaiModelLogoDark from "./assets/image/models/rakutenai_dark.png"; import SparkDeskModelLogo from "./assets/image/models/sparkdesk.png"; import SparkDeskModelLogoDark from "./assets/image/models/sparkdesk_dark.png"; import StabilityModelLogo from "./assets/image/models/stability.png"; import StabilityModelLogoDark from "./assets/image/models/stability_dark.png"; import StepModelLogo from "./assets/image/models/step.png"; import StepModelLogoDark from "./assets/image/models/step_dark.png"; import SunoModelLogo from "./assets/image/models/suno.png"; import SunoModelLogoDark from "./assets/image/models/suno_dark.png"; import TeleModelLogo from "./assets/image/models/tele.png"; import TeleModelLogoDark from "./assets/image/models/tele_dark.png"; import UpstageModelLogo from "./assets/image/models/upstage.png"; import UpstageModelLogoDark from "./assets/image/models/upstage_dark.png"; import ViduModelLogo from "./assets/image/models/vidu.png"; import ViduModelLogoDark from "./assets/image/models/vidu_dark.png"; import VoyageModelLogo from "./assets/image/models/voyageai.png"; import WenxinModelLogo from "./assets/image/models/wenxin.png"; import WenxinModelLogoDark from "./assets/image/models/wenxin_dark.png"; import XirangModelLogo from "./assets/image/models/xirang.png"; import XirangModelLogoDark from "./assets/image/models/xirang_dark.png"; import YiModelLogo from "./assets/image/models/yi.png"; import YiModelLogoDark from "./assets/image/models/yi_dark.png"; import { Model } from "./types"; export function getModelLogo(modelId: string) { const isLight = true; if (!modelId) { return undefined; } const logoMap = { pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark, jina: isLight ? JinaModelLogo : JinaModelLogoDark, abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark, minimax: isLight ? MinimaxModelLogo : MinimaxModelLogoDark, o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, "gpt-3": isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark, "gpt-4": isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark, gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark, "text-moderation": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "babbage-": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "sora-": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "(^|/)omni-": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "Embedding-V1": isLight ? WenxinModelLogo : WenxinModelLogoDark, "text-embedding-v": isLight ? QwenModelLogo : QwenModelLogoDark, "text-embedding": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "davinci-": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark, deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark, "(qwen|qwq-|qvq-)": isLight ? QwenModelLogo : QwenModelLogoDark, gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark, "yi-": isLight ? YiModelLogo : YiModelLogoDark, llama: isLight ? LlamaModelLogo : LlamaModelLogoDark, mixtral: isLight ? MistralModelLogo : MistralModelLogo, mistral: isLight ? MistralModelLogo : MistralModelLogoDark, codestral: CodestralModelLogo, ministral: isLight ? MistralModelLogo : MistralModelLogoDark, moonshot: isLight ? MoonshotModelLogo : MoonshotModelLogoDark, kimi: isLight ? MoonshotModelLogo : MoonshotModelLogoDark, phi: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark, baichuan: isLight ? BaichuanModelLogo : BaichuanModelLogoDark, claude: isLight ? ClaudeModelLogo : ClaudeModelLogoDark, gemini: isLight ? GeminiModelLogo : GeminiModelLogoDark, bison: isLight ? PalmModelLogo : PalmModelLogoDark, palm: isLight ? PalmModelLogo : PalmModelLogoDark, step: isLight ? StepModelLogo : StepModelLogoDark, hailuo: isLight ? HailuoModelLogo : HailuoModelLogoDark, doubao: isLight ? DoubaoModelLogo : DoubaoModelLogoDark, "ep-202": isLight ? DoubaoModelLogo : DoubaoModelLogoDark, cohere: isLight ? CohereModelLogo : CohereModelLogoDark, command: isLight ? CohereModelLogo : CohereModelLogoDark, minicpm: isLight ? MinicpmModelLogo : MinicpmModelLogoDark, "360": isLight ? Ai360ModelLogo : Ai360ModelLogoDark, aimass: isLight ? AimassModelLogo : AimassModelLogoDark, codegeex: isLight ? CodegeexModelLogo : CodegeexModelLogoDark, copilot: isLight ? CopilotModelLogo : CopilotModelLogoDark, creative: isLight ? CopilotModelLogo : CopilotModelLogoDark, balanced: isLight ? CopilotModelLogo : CopilotModelLogoDark, precise: isLight ? CopilotModelLogo : CopilotModelLogoDark, dalle: isLight ? DalleModelLogo : DalleModelLogoDark, "dall-e": isLight ? DalleModelLogo : DalleModelLogoDark, dbrx: isLight ? DbrxModelLogo : DbrxModelLogo, flashaudio: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark, flux: isLight ? FluxModelLogo : FluxModelLogoDark, grok: isLight ? GrokModelLogo : GrokModelLogoDark, hunyuan: isLight ? HunyuanModelLogo : HunyuanModelLogoDark, internlm: isLight ? InternlmModelLogo : InternlmModelLogoDark, internvl: InternvlModelLogo, llava: isLight ? LLavaModelLogo : LLavaModelLogoDark, magic: isLight ? MagicModelLogo : MagicModelLogoDark, midjourney: isLight ? MidjourneyModelLogo : MidjourneyModelLogoDark, "mj-": isLight ? MidjourneyModelLogo : MidjourneyModelLogoDark, "tao-": isLight ? WenxinModelLogo : WenxinModelLogoDark, "ernie-": isLight ? WenxinModelLogo : WenxinModelLogoDark, voice: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark, "tts-1": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "whisper-": isLight ? ChatGptModelLogo : ChatGptModelLogoDark, "stable-": isLight ? StabilityModelLogo : StabilityModelLogoDark, sd2: isLight ? StabilityModelLogo : StabilityModelLogoDark, sd3: isLight ? StabilityModelLogo : StabilityModelLogoDark, sdxl: isLight ? StabilityModelLogo : StabilityModelLogoDark, sparkdesk: isLight ? SparkDeskModelLogo : SparkDeskModelLogoDark, generalv: isLight ? SparkDeskModelLogo : SparkDeskModelLogoDark, wizardlm: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark, microsoft: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark, hermes: isLight ? NousResearchModelLogo : NousResearchModelLogoDark, gryphe: isLight ? GrypheModelLogo : GrypheModelLogoDark, suno: isLight ? SunoModelLogo : SunoModelLogoDark, chirp: isLight ? SunoModelLogo : SunoModelLogoDark, luma: isLight ? LumaModelLogo : LumaModelLogoDark, keling: isLight ? KeLingModelLogo : KeLingModelLogoDark, "vidu-": isLight ? ViduModelLogo : ViduModelLogoDark, ai21: isLight ? Ai21ModelLogo : Ai21ModelLogoDark, "jamba-": isLight ? Ai21ModelLogo : Ai21ModelLogoDark, mythomax: isLight ? GrypheModelLogo : GrypheModelLogoDark, nvidia: isLight ? NvidiaModelLogo : NvidiaModelLogoDark, dianxin: isLight ? DianxinModelLogo : DianxinModelLogoDark, tele: isLight ? TeleModelLogo : TeleModelLogoDark, adept: isLight ? AdeptModelLogo : AdeptModelLogoDark, aisingapore: isLight ? AisingaporeModelLogo : AisingaporeModelLogoDark, bigcode: isLight ? BigcodeModelLogo : BigcodeModelLogoDark, mediatek: isLight ? MediatekModelLogo : MediatekModelLogoDark, upstage: isLight ? UpstageModelLogo : UpstageModelLogoDark, rakutenai: isLight ? RakutenaiModelLogo : RakutenaiModelLogoDark, ibm: isLight ? IbmModelLogo : IbmModelLogoDark, "google/": isLight ? GoogleModelLogo : GoogleModelLogoDark, xirang: isLight ? XirangModelLogo : XirangModelLogoDark, hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark, embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark, perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark, sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark, "bge-": BgeModelLogo, "voyage-": VoyageModelLogo, }; for (const key in logoMap) { const regex = new RegExp(key, "i"); if (regex.test(modelId)) { return logoMap[key]; } } return isLight ? ChatGptModelLogo : ChatGptModelLogoDark; } export const SystemModels: Record[]> = { aihubmix: [ { id: "gpt-4o", provider: "aihubmix", name: "GPT-4o", group: "GPT-4o", }, { id: "claude-3-5-sonnet-latest", provider: "aihubmix", name: "Claude 3.5 Sonnet", group: "Claude 3.5", }, { id: "gemini-2.0-flash-exp-search", provider: "aihubmix", name: "Gemini 2.0 Flash Exp Search", group: "Gemini 2.0", }, { id: "deepseek-chat", provider: "aihubmix", name: "DeepSeek Chat", group: "DeepSeek Chat", }, { id: "aihubmix-Llama-3-3-70B-Instruct", provider: "aihubmix", name: "Llama-3.3-70b", group: "Llama 3.3", }, { id: "Qwen/QVQ-72B-Preview", provider: "aihubmix", name: "Qwen/QVQ-72B", group: "Qwen", }, ], o3: [ { id: "gpt-4o", provider: "o3", name: "GPT-4o", group: "OpenAI", }, { id: "o1-mini", provider: "o3", name: "o1-mini", group: "OpenAI", }, { id: "o1-preview", provider: "o3", name: "o1-preview", group: "OpenAI", }, { id: "o3-mini", provider: "o3", name: "o3-mini", group: "OpenAI", }, { id: "o3-mini-high", provider: "o3", name: "o3-mini-high", group: "OpenAI", }, { id: "claude-3-7-sonnet-20250219", provider: "o3", name: "claude-3-7-sonnet-20250219", group: "Anthropic", }, { id: "claude-3-5-sonnet-20241022", provider: "o3", name: "claude-3-5-sonnet-20241022", group: "Anthropic", }, { id: "claude-3-5-haiku-20241022", provider: "o3", name: "claude-3-5-haiku-20241022", group: "Anthropic", }, { id: "claude-3-opus-20240229", provider: "o3", name: "claude-3-opus-20240229", group: "Anthropic", }, { id: "claude-3-haiku-20240307", provider: "o3", name: "claude-3-haiku-20240307", group: "Anthropic", }, { id: "claude-3-5-sonnet-20240620", provider: "o3", name: "claude-3-5-sonnet-20240620", group: "Anthropic", }, { id: "deepseek-ai/Deepseek-R1", provider: "o3", name: "DeepSeek R1", group: "DeepSeek", }, { id: "deepseek-reasoner", provider: "o3", name: "deepseek-reasoner", group: "DeepSeek", }, { id: "deepseek-chat", provider: "o3", name: "deepseek-chat", group: "DeepSeek", }, { id: "deepseek-ai/DeepSeek-V3", provider: "o3", name: "DeepSeek V3", group: "DeepSeek", }, { id: "text-embedding-3-small", provider: "o3", name: "text-embedding-3-small", group: "model.embedModels", }, { id: "text-embedding-ada-002", provider: "o3", name: "text-embedding-ada-002", group: "model.embedModels", }, { id: "text-embedding-v2", provider: "o3", name: "text-embedding-v2", group: "model.embedModels", }, { id: "Doubao-embedding", provider: "o3", name: "Doubao-embedding", group: "model.embedModels", }, { id: "Doubao-embedding-large", provider: "o3", name: "Doubao-embedding-large", group: "model.embedModels", }, ], ollama: [], lmstudio: [], silicon: [ { id: "deepseek-ai/DeepSeek-R1", name: "deepseek-ai/DeepSeek-R1", provider: "silicon", group: "deepseek-ai", }, { id: "deepseek-ai/DeepSeek-V3", name: "deepseek-ai/DeepSeek-V3", provider: "silicon", group: "deepseek-ai", }, { id: "Qwen/Qwen2.5-7B-Instruct", provider: "silicon", name: "Qwen2.5-7B-Instruct", group: "Qwen", }, { id: "meta-llama/Llama-3.3-70B-Instruct", name: "meta-llama/Llama-3.3-70B-Instruct", provider: "silicon", group: "meta-llama", }, { id: "BAAI/bge-m3", name: "BAAI/bge-m3", provider: "silicon", group: "BAAI", }, ], ppio: [ { id: "deepseek/deepseek-r1/community", name: "DeepSeek: DeepSeek R1 (Community)", provider: "ppio", group: "deepseek", }, { id: "deepseek/deepseek-v3/community", name: "DeepSeek: DeepSeek V3 (Community)", provider: "ppio", group: "deepseek", }, { id: "deepseek/deepseek-r1", provider: "ppio", name: "DeepSeek R1", group: "deepseek", }, { id: "deepseek/deepseek-v3", provider: "ppio", name: "DeepSeek V3", group: "deepseek", }, { id: "qwen/qwen-2.5-72b-instruct", provider: "ppio", name: "Qwen2.5-72B-Instruct", group: "qwen", }, { id: "qwen/qwen2.5-32b-instruct", provider: "ppio", name: "Qwen2.5-32B-Instruct", group: "qwen", }, { id: "meta-llama/llama-3.1-70b-instruct", provider: "ppio", name: "Llama-3.1-70B-Instruct", group: "meta-llama", }, { id: "meta-llama/llama-3.1-8b-instruct", provider: "ppio", name: "Llama-3.1-8B-Instruct", group: "meta-llama", }, { id: "01-ai/yi-1.5-34b-chat", provider: "ppio", name: "Yi-1.5-34B-Chat", group: "01-ai", }, { id: "01-ai/yi-1.5-9b-chat", provider: "ppio", name: "Yi-1.5-9B-Chat", group: "01-ai", }, ], alayanew: [], openai: [ { id: "gpt-4.5-preview", provider: "openai", name: "gpt-4.5-preview", group: "gpt-4.5", }, { id: "gpt-4o", provider: "openai", name: "GPT-4o", group: "GPT 4o" }, { id: "gpt-4o-mini", provider: "openai", name: "GPT-4o-mini", group: "GPT 4o", }, { id: "o1-mini", provider: "openai", name: "o1-mini", group: "o1" }, { id: "o1-preview", provider: "openai", name: "o1-preview", group: "o1", }, ], "azure-openai": [ { id: "gpt-4o", provider: "azure-openai", name: "GPT-4o", group: "GPT 4o", }, { id: "gpt-4o-mini", provider: "azure-openai", name: "GPT-4o-mini", group: "GPT 4o", }, ], gemini: [ { id: "gemini-1.5-flash", provider: "gemini", name: "Gemini 1.5 Flash", group: "Gemini 1.5", }, { id: "gemini-1.5-flash-8b", provider: "gemini", name: "Gemini 1.5 Flash (8B)", group: "Gemini 1.5", }, { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro", provider: "gemini", group: "Gemini 1.5", }, { id: "gemini-2.0-flash", provider: "gemini", name: "Gemini 2.0 Flash", group: "Gemini 2.0", }, ], anthropic: [ { id: "claude-3-7-sonnet-20250219", provider: "anthropic", name: "Claude 3.7 Sonnet", group: "Claude 3.7", }, { id: "claude-3-5-sonnet-20241022", provider: "anthropic", name: "Claude 3.5 Sonnet", group: "Claude 3.5", }, { id: "claude-3-5-haiku-20241022", provider: "anthropic", name: "Claude 3.5 Haiku", group: "Claude 3.5", }, { id: "claude-3-5-sonnet-20240620", provider: "anthropic", name: "Claude 3.5 Sonnet (Legacy)", group: "Claude 3.5", }, { id: "claude-3-opus-20240229", provider: "anthropic", name: "Claude 3 Opus", group: "Claude 3", }, { id: "claude-3-haiku-20240307", provider: "anthropic", name: "Claude 3 Haiku", group: "Claude 3", }, ], "gitee-ai": [ { id: "DeepSeek-R1-Distill-Qwen-32B", name: "DeepSeek-R1-Distill-Qwen-32B", provider: "gitee-ai", group: "DeepSeek", }, { id: "DeepSeek-R1-Distill-Qwen-1.5B", name: "DeepSeek-R1-Distill-Qwen-1.5B", provider: "gitee-ai", group: "DeepSeek", }, { id: "DeepSeek-R1-Distill-Qwen-14B", name: "DeepSeek-R1-Distill-Qwen-14B", provider: "gitee-ai", group: "DeepSeek", }, { id: "DeepSeek-R1-Distill-Qwen-7B", name: "DeepSeek-R1-Distill-Qwen-7B", provider: "gitee-ai", group: "DeepSeek", }, { id: "DeepSeek-V3", name: "DeepSeek-V3", provider: "gitee-ai", group: "DeepSeek", }, { id: "DeepSeek-R1", name: "DeepSeek-R1", provider: "gitee-ai", group: "DeepSeek", }, { id: "deepseek-coder-33B-instruct", name: "deepseek-coder-33B-instruct", provider: "gitee-ai", group: "DeepSeek", }, { id: "Qwen2.5-72B-Instruct", name: "Qwen2.5-72B-Instruct", provider: "gitee-ai", group: "Qwen", }, { id: "Qwen2.5-14B-Instruct", name: "Qwen2.5-14B-Instruct", provider: "gitee-ai", group: "Qwen", }, { id: "Qwen2-7B-Instruct", name: "Qwen2-7B-Instruct", provider: "gitee-ai", group: "Qwen", }, { id: "Qwen2.5-32B-Instruct", name: "Qwen2.5-32B-Instruct", provider: "gitee-ai", group: "Qwen", }, { id: "Qwen2-72B-Instruct", name: "Qwen2-72B-Instruct", provider: "gitee-ai", group: "Qwen", }, { id: "Qwen2-VL-72B", name: "Qwen2-VL-72B", provider: "gitee-ai", group: "Qwen", }, { id: "QwQ-32B-Preview", name: "QwQ-32B-Preview", provider: "gitee-ai", group: "Qwen", }, { id: "Yi-34B-Chat", name: "Yi-34B-Chat", provider: "gitee-ai", group: "01-ai", }, { id: "glm-4-9b-chat", name: "glm-4-9b-chat", provider: "gitee-ai", group: "THUDM", }, { id: "codegeex4-all-9b", name: "codegeex4-all-9b", provider: "gitee-ai", group: "THUDM", }, { id: "InternVL2-8B", name: "InternVL2-8B", provider: "gitee-ai", group: "OpenGVLab", }, { id: "InternVL2.5-26B", name: "InternVL2.5-26B", provider: "gitee-ai", group: "OpenGVLab", }, { id: "InternVL2.5-78B", name: "InternVL2.5-78B", provider: "gitee-ai", group: "OpenGVLab", }, { id: "bge-large-zh-v1.5", name: "bge-large-zh-v1.5", provider: "gitee-ai", group: "BAAI", }, { id: "bge-small-zh-v1.5", name: "bge-small-zh-v1.5", provider: "gitee-ai", group: "BAAI", }, { id: "bge-m3", name: "bge-m3", provider: "gitee-ai", group: "BAAI", }, { id: "bce-embedding-base_v1", name: "bce-embedding-base_v1", provider: "gitee-ai", group: "netease-youdao", }, ], deepseek: [ { id: "deepseek-chat", provider: "deepseek", name: "DeepSeek Chat", group: "DeepSeek Chat", }, { id: "deepseek-reasoner", provider: "deepseek", name: "DeepSeek Reasoner", group: "DeepSeek Reasoner", }, ], together: [ { id: "meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo", provider: "together", name: "Llama-3.2-11B-Vision", group: "Llama-3.2", }, { id: "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo", provider: "together", name: "Llama-3.2-90B-Vision", group: "Llama-3.2", }, { id: "google/gemma-2-27b-it", provider: "together", name: "gemma-2-27b-it", group: "Gemma", }, { id: "google/gemma-2-9b-it", provider: "together", name: "gemma-2-9b-it", group: "Gemma", }, ], ocoolai: [ { id: "deepseek-chat", provider: "ocoolai", name: "deepseek-chat", group: "DeepSeek", }, { id: "deepseek-reasoner", provider: "ocoolai", name: "deepseek-reasoner", group: "DeepSeek", }, { id: "deepseek-ai/DeepSeek-R1", provider: "ocoolai", name: "deepseek-ai/DeepSeek-R1", group: "DeepSeek", }, { id: "HiSpeed/DeepSeek-R1", provider: "ocoolai", name: "HiSpeed/DeepSeek-R1", group: "DeepSeek", }, { id: "ocoolAI/DeepSeek-R1", provider: "ocoolai", name: "ocoolAI/DeepSeek-R1", group: "DeepSeek", }, { id: "Azure/DeepSeek-R1", provider: "ocoolai", name: "Azure/DeepSeek-R1", group: "DeepSeek", }, { id: "gpt-4o", provider: "ocoolai", name: "gpt-4o", group: "OpenAI", }, { id: "gpt-4o-all", provider: "ocoolai", name: "gpt-4o-all", group: "OpenAI", }, { id: "gpt-4o-mini", provider: "ocoolai", name: "gpt-4o-mini", group: "OpenAI", }, { id: "gpt-4", provider: "ocoolai", name: "gpt-4", group: "OpenAI", }, { id: "o1-preview", provider: "ocoolai", name: "o1-preview", group: "OpenAI", }, { id: "o1-mini", provider: "ocoolai", name: "o1-mini", group: "OpenAI", }, { id: "claude-3-5-sonnet-20240620", provider: "ocoolai", name: "claude-3-5-sonnet-20240620", group: "Anthropic", }, { id: "claude-3-5-haiku-20241022", provider: "ocoolai", name: "claude-3-5-haiku-20241022", group: "Anthropic", }, { id: "gemini-pro", provider: "ocoolai", name: "gemini-pro", group: "Gemini", }, { id: "gemini-1.5-pro", provider: "ocoolai", name: "gemini-1.5-pro", group: "Gemini", }, { id: "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo", provider: "ocoolai", name: "Llama-3.2-90B-Vision-Instruct-Turbo", group: "Llama-3.2", }, { id: "meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo", provider: "ocoolai", name: "Llama-3.2-11B-Vision-Instruct-Turbo", group: "Llama-3.2", }, { id: "meta-llama/Llama-3.2-3B-Vision-Instruct-Turbo", provider: "ocoolai", name: "Llama-3.2-3B-Vision-Instruct-Turbo", group: "Llama-3.2", }, { id: "google/gemma-2-27b-it", provider: "ocoolai", name: "gemma-2-27b-it", group: "Gemma", }, { id: "google/gemma-2-9b-it", provider: "ocoolai", name: "gemma-2-9b-it", group: "Gemma", }, { id: "Doubao-embedding", provider: "ocoolai", name: "Doubao-embedding", group: "Doubao", }, { id: "text-embedding-3-large", provider: "ocoolai", name: "text-embedding-3-large", group: "Embedding", }, { id: "text-embedding-3-small", provider: "ocoolai", name: "text-embedding-3-small", group: "Embedding", }, { id: "text-embedding-v2", provider: "ocoolai", name: "text-embedding-v2", group: "Embedding", }, ], github: [ { id: "gpt-4o", provider: "github", name: "OpenAI GPT-4o", group: "OpenAI", }, ], copilot: [ { id: "gpt-4o-mini", provider: "copilot", name: "OpenAI GPT-4o-mini", group: "OpenAI", }, ], yi: [ { id: "yi-lightning", name: "Yi Lightning", provider: "yi", group: "yi-lightning", }, { id: "yi-vision-v2", name: "Yi Vision v2", provider: "yi", group: "yi-vision", }, ], zhipu: [ { id: "glm-zero-preview", provider: "zhipu", name: "GLM-Zero-Preview", group: "GLM-Zero", }, { id: "glm-4-0520", provider: "zhipu", name: "GLM-4-0520", group: "GLM-4", }, { id: "glm-4-long", provider: "zhipu", name: "GLM-4-Long", group: "GLM-4", }, { id: "glm-4-plus", provider: "zhipu", name: "GLM-4-Plus", group: "GLM-4", }, { id: "glm-4-air", provider: "zhipu", name: "GLM-4-Air", group: "GLM-4", }, { id: "glm-4-airx", provider: "zhipu", name: "GLM-4-AirX", group: "GLM-4", }, { id: "glm-4-flash", provider: "zhipu", name: "GLM-4-Flash", group: "GLM-4", }, { id: "glm-4-flashx", provider: "zhipu", name: "GLM-4-FlashX", group: "GLM-4", }, { id: "glm-4v", provider: "zhipu", name: "GLM 4V", group: "GLM-4v", }, { id: "glm-4v-flash", provider: "zhipu", name: "GLM-4V-Flash", group: "GLM-4v", }, { id: "glm-4v-plus", provider: "zhipu", name: "GLM-4V-Plus", group: "GLM-4v", }, { id: "glm-4-alltools", provider: "zhipu", name: "GLM-4-AllTools", group: "GLM-4-AllTools", }, { id: "embedding-3", provider: "zhipu", name: "Embedding-3", group: "Embedding", }, ], moonshot: [ { id: "moonshot-v1-auto", name: "moonshot-v1-auto", provider: "moonshot", group: "moonshot-v1", }, ], baichuan: [ { id: "Baichuan4", provider: "baichuan", name: "Baichuan4", group: "Baichuan4", }, { id: "Baichuan3-Turbo", provider: "baichuan", name: "Baichuan3 Turbo", group: "Baichuan3", }, { id: "Baichuan3-Turbo-128k", provider: "baichuan", name: "Baichuan3 Turbo 128k", group: "Baichuan3", }, ], modelscope: [ { id: "Qwen/Qwen2.5-72B-Instruct", name: "Qwen/Qwen2.5-72B-Instruct", provider: "modelscope", group: "Qwen", }, { id: "Qwen/Qwen2.5-VL-72B-Instruct", name: "Qwen/Qwen2.5-VL-72B-Instruct", provider: "modelscope", group: "Qwen", }, { id: "Qwen/Qwen2.5-Coder-32B-Instruct", name: "Qwen/Qwen2.5-Coder-32B-Instruct", provider: "modelscope", group: "Qwen", }, { id: "deepseek-ai/DeepSeek-R1", name: "deepseek-ai/DeepSeek-R1", provider: "modelscope", group: "deepseek-ai", }, { id: "deepseek-ai/DeepSeek-V3", name: "deepseek-ai/DeepSeek-V3", provider: "modelscope", group: "deepseek-ai", }, ], bailian: [ { id: "qwen-vl-plus", name: "qwen-vl-plus", provider: "dashscope", group: "qwen-vl", }, { id: "qwen-coder-plus", name: "qwen-coder-plus", provider: "dashscope", group: "qwen-coder", }, { id: "qwen-turbo", name: "qwen-turbo", provider: "dashscope", group: "qwen-turbo", }, { id: "qwen-plus", name: "qwen-plus", provider: "dashscope", group: "qwen-plus", }, { id: "qwen-max", name: "qwen-max", provider: "dashscope", group: "qwen-max", }, ], stepfun: [ { id: "step-1-8k", provider: "stepfun", name: "Step 1 8K", group: "Step 1", }, { id: "step-1-flash", provider: "stepfun", name: "Step 1 Flash", group: "Step 1", }, ], doubao: [], minimax: [ { id: "abab6.5s-chat", provider: "minimax", name: "abab6.5s", group: "abab6", }, { id: "abab6.5g-chat", provider: "minimax", name: "abab6.5g", group: "abab6", }, { id: "abab6.5t-chat", provider: "minimax", name: "abab6.5t", group: "abab6", }, { id: "abab5.5s-chat", provider: "minimax", name: "abab5.5s", group: "abab5", }, { id: "minimax-text-01", provider: "minimax", name: "minimax-01", group: "minimax-01", }, ], hyperbolic: [ { id: "Qwen/Qwen2-VL-72B-Instruct", provider: "hyperbolic", name: "Qwen2-VL-72B-Instruct", group: "Qwen2-VL", }, { id: "Qwen/Qwen2-VL-7B-Instruct", provider: "hyperbolic", name: "Qwen2-VL-7B-Instruct", group: "Qwen2-VL", }, { id: "mistralai/Pixtral-12B-2409", provider: "hyperbolic", name: "Pixtral-12B-2409", group: "Pixtral", }, { id: "meta-llama/Meta-Llama-3.1-405B", provider: "hyperbolic", name: "Meta-Llama-3.1-405B", group: "Meta-Llama-3.1", }, ], grok: [ { id: "grok-beta", provider: "grok", name: "Grok Beta", group: "Grok", }, { id: "grok-vision-beta", provider: "grok", name: "Grok Vision Beta", group: "Grok", }, ], mistral: [ { id: "pixtral-12b-2409", provider: "mistral", name: "Pixtral 12B [Free]", group: "Pixtral", }, { id: "pixtral-large-latest", provider: "mistral", name: "Pixtral Large", group: "Pixtral", }, { id: "ministral-3b-latest", provider: "mistral", name: "Mistral 3B [Free]", group: "Mistral Mini", }, { id: "ministral-8b-latest", provider: "mistral", name: "Mistral 8B [Free]", group: "Mistral Mini", }, { id: "codestral-latest", provider: "mistral", name: "Mistral Codestral", group: "Mistral Code", }, { id: "mistral-large-latest", provider: "mistral", name: "Mistral Large", group: "Mistral Chat", }, { id: "mistral-small-latest", provider: "mistral", name: "Mistral Small", group: "Mistral Chat", }, { id: "open-mistral-nemo", provider: "mistral", name: "Mistral Nemo", group: "Mistral Chat", }, { id: "mistral-embed", provider: "mistral", name: "Mistral Embedding", group: "Mistral Embed", }, ], jina: [ { id: "jina-clip-v1", provider: "jina", name: "jina-clip-v1", group: "Jina Clip", }, { id: "jina-clip-v2", provider: "jina", name: "jina-clip-v2", group: "Jina Clip", }, { id: "jina-embeddings-v2-base-en", provider: "jina", name: "jina-embeddings-v2-base-en", group: "Jina Embeddings V2", }, { id: "jina-embeddings-v2-base-es", provider: "jina", name: "jina-embeddings-v2-base-es", group: "Jina Embeddings V2", }, { id: "jina-embeddings-v2-base-de", provider: "jina", name: "jina-embeddings-v2-base-de", group: "Jina Embeddings V2", }, { id: "jina-embeddings-v2-base-zh", provider: "jina", name: "jina-embeddings-v2-base-zh", group: "Jina Embeddings V2", }, { id: "jina-embeddings-v2-base-code", provider: "jina", name: "jina-embeddings-v2-base-code", group: "Jina Embeddings V2", }, { id: "jina-embeddings-v3", provider: "jina", name: "jina-embeddings-v3", group: "Jina Embeddings V3", }, ], fireworks: [ { id: "accounts/fireworks/models/mythomax-l2-13b", provider: "fireworks", name: "mythomax-l2-13b", group: "Gryphe", }, { id: "accounts/fireworks/models/llama-v3-70b-instruct", provider: "fireworks", name: "Llama-3-70B-Instruct", group: "Llama3", }, ], zhinao: [ { id: "360gpt-pro", provider: "zhinao", name: "360gpt-pro", group: "360Gpt", }, { id: "360gpt-turbo", provider: "zhinao", name: "360gpt-turbo", group: "360Gpt", }, ], hunyuan: [ { id: "hunyuan-pro", provider: "hunyuan", name: "hunyuan-pro", group: "Hunyuan", }, { id: "hunyuan-standard", provider: "hunyuan", name: "hunyuan-standard", group: "Hunyuan", }, { id: "hunyuan-lite", provider: "hunyuan", name: "hunyuan-lite", group: "Hunyuan", }, { id: "hunyuan-standard-256k", provider: "hunyuan", name: "hunyuan-standard-256k", group: "Hunyuan", }, { id: "hunyuan-vision", provider: "hunyuan", name: "hunyuan-vision", group: "Hunyuan", }, { id: "hunyuan-code", provider: "hunyuan", name: "hunyuan-code", group: "Hunyuan", }, { id: "hunyuan-role", provider: "hunyuan", name: "hunyuan-role", group: "Hunyuan", }, { id: "hunyuan-turbo", provider: "hunyuan", name: "hunyuan-turbo", group: "Hunyuan", }, { id: "hunyuan-turbos-latest", provider: "hunyuan", name: "hunyuan-turbos-latest", group: "Hunyuan", }, { id: "hunyuan-embedding", provider: "hunyuan", name: "hunyuan-embedding", group: "Embedding", }, ], nvidia: [ { id: "01-ai/yi-large", provider: "nvidia", name: "yi-large", group: "Yi", }, { id: "meta/llama-3.1-405b-instruct", provider: "nvidia", name: "llama-3.1-405b-instruct", group: "llama-3.1", }, ], openrouter: [ { id: "google/gemma-2-9b-it:free", provider: "openrouter", name: "Google: Gemma 2 9B", group: "Gemma", }, { id: "microsoft/phi-3-mini-128k-instruct:free", provider: "openrouter", name: "Phi-3 Mini 128K Instruct", group: "Phi", }, { id: "microsoft/phi-3-medium-128k-instruct:free", provider: "openrouter", name: "Phi-3 Medium 128K Instruct", group: "Phi", }, { id: "meta-llama/llama-3-8b-instruct:free", provider: "openrouter", name: "Meta: Llama 3 8B Instruct", group: "Llama3", }, { id: "mistralai/mistral-7b-instruct:free", provider: "openrouter", name: "Mistral: Mistral 7B Instruct", group: "Mistral", }, ], groq: [ { id: "llama3-8b-8192", provider: "groq", name: "LLaMA3 8B", group: "Llama3", }, { id: "llama3-70b-8192", provider: "groq", name: "LLaMA3 70B", group: "Llama3", }, { id: "mistral-saba-24b", provider: "groq", name: "Mistral Saba 24B", group: "Mistral", }, { id: "gemma-9b-it", provider: "groq", name: "Gemma 9B", group: "Gemma", }, ], "baidu-cloud": [ { id: "deepseek-r1", provider: "baidu-cloud", name: "DeepSeek R1", group: "DeepSeek", }, { id: "deepseek-v3", provider: "baidu-cloud", name: "DeepSeek V3", group: "DeepSeek", }, { id: "ernie-4.0-8k-latest", provider: "baidu-cloud", name: "ERNIE-4.0", group: "ERNIE", }, { id: "ernie-4.0-turbo-8k-latest", provider: "baidu-cloud", name: "ERNIE 4.0 Trubo", group: "ERNIE", }, { id: "ernie-speed-8k", provider: "baidu-cloud", name: "ERNIE Speed", group: "ERNIE", }, { id: "ernie-lite-8k", provider: "baidu-cloud", name: "ERNIE Lite", group: "ERNIE", }, { id: "bge-large-zh", provider: "baidu-cloud", name: "BGE Large ZH", group: "Embedding", }, { id: "bge-large-en", provider: "baidu-cloud", name: "BGE Large EN", group: "Embedding", }, ], dmxapi: [ { id: "Qwen/Qwen2.5-7B-Instruct", provider: "dmxapi", name: "Qwen/Qwen2.5-7B-Instruct", group: "model.freeModels", }, { id: "ERNIE-Speed-128K", provider: "dmxapi", name: "ERNIE-Speed-128K", group: "model.freeModels", }, { id: "THUDM/glm-4-9b-chat", provider: "dmxapi", name: "THUDM/glm-4-9b-chat", group: "model.freeModels", }, { id: "glm-4-flash", provider: "dmxapi", name: "glm-4-flash", group: "model.freeModels", }, { id: "hunyuan-lite", provider: "dmxapi", name: "hunyuan-lite", group: "model.freeModels", }, { id: "gpt-4o", provider: "dmxapi", name: "gpt-4o", group: "OpenAI", }, { id: "gpt-4o-mini", provider: "dmxapi", name: "gpt-4o-mini", group: "OpenAI", }, { id: "DMXAPI-DeepSeek-R1", provider: "dmxapi", name: "DMXAPI-DeepSeek-R1", group: "DeepSeek", }, { id: "DMXAPI-DeepSeek-V3", provider: "dmxapi", name: "DMXAPI-DeepSeek-V3", group: "DeepSeek", }, { id: "claude-3-5-sonnet-20241022", provider: "dmxapi", name: "claude-3-5-sonnet-20241022", group: "Claude", }, { id: "gemini-2.0-flash", provider: "dmxapi", name: "gemini-2.0-flash", group: "Gemini", }, ], perplexity: [ { id: "sonar-reasoning-pro", provider: "perplexity", name: "sonar-reasoning-pro", group: "Sonar", }, { id: "sonar-reasoning", provider: "perplexity", name: "sonar-reasoning", group: "Sonar", }, { id: "sonar-pro", provider: "perplexity", name: "sonar-pro", group: "Sonar", }, { id: "sonar", provider: "perplexity", name: "sonar", group: "Sonar", }, ], infini: [ { id: "deepseek-r1", provider: "infini", name: "deepseek-r1", group: "DeepSeek", }, { id: "deepseek-r1-distill-qwen-32b", provider: "infini", name: "deepseek-r1-distill-qwen-32b", group: "DeepSeek", }, { id: "deepseek-v3", provider: "infini", name: "deepseek-v3", group: "DeepSeek", }, { id: "qwen2.5-72b-instruct", provider: "infini", name: "qwen2.5-72b-instruct", group: "Qwen", }, { id: "qwen2.5-32b-instruct", provider: "infini", name: "qwen2.5-32b-instruct", group: "Qwen", }, { id: "qwen2.5-14b-instruct", provider: "infini", name: "qwen2.5-14b-instruct", group: "Qwen", }, { id: "qwen2.5-7b-instruct", provider: "infini", name: "qwen2.5-7b-instruct", group: "Qwen", }, { id: "qwen2-72b-instruct", provider: "infini", name: "qwen2-72b-instruct", group: "Qwen", }, { id: "qwq-32b-preview", provider: "infini", name: "qwq-32b-preview", group: "Qwen", }, { id: "qwen2.5-coder-32b-instruct", provider: "infini", name: "qwen2.5-coder-32b-instruct", group: "Qwen", }, { id: "llama-3.3-70b-instruct", provider: "infini", name: "llama-3.3-70b-instruct", group: "Llama", }, { id: "bge-m3", provider: "infini", name: "bge-m3", group: "BAAI", }, { id: "gemma-2-27b-it", provider: "infini", name: "gemma-2-27b-it", group: "Gemma", }, { id: "jina-embeddings-v2-base-zh", provider: "infini", name: "jina-embeddings-v2-base-zh", group: "Jina", }, { id: "jina-embeddings-v2-base-code", provider: "infini", name: "jina-embeddings-v2-base-code", group: "Jina", }, ], xirang: [], "tencent-cloud-ti": [ { id: "deepseek-r1", provider: "tencent-cloud-ti", name: "DeepSeek R1", group: "DeepSeek", }, { id: "deepseek-v3", provider: "tencent-cloud-ti", name: "DeepSeek V3", group: "DeepSeek", }, ], gpustack: [], voyageai: [ { id: "voyage-3-large", provider: "voyageai", name: "voyage-3-large", group: "Voyage Embeddings V3", }, { id: "voyage-3", provider: "voyageai", name: "voyage-3", group: "Voyage Embeddings V3", }, { id: "voyage-3-lite", provider: "voyageai", name: "voyage-3-lite", group: "Voyage Embeddings V3", }, { id: "voyage-code-3", provider: "voyageai", name: "voyage-code-3", group: "Voyage Embeddings V3", }, { id: "voyage-finance-3", provider: "voyageai", name: "voyage-finance-3", group: "Voyage Embeddings V2", }, { id: "voyage-law-2", provider: "voyageai", name: "voyage-law-2", group: "Voyage Embeddings V2", }, { id: "voyage-code-2", provider: "voyageai", name: "voyage-code-2", group: "Voyage Embeddings V2", }, { id: "rerank-2", provider: "voyageai", name: "rerank-2", group: "Voyage Rerank V2", }, { id: "rerank-2-lite", provider: "voyageai", name: "rerank-2-lite", group: "Voyage Rerank V2", }, ], }; ================================================ FILE: src/module/Model/provider/driver/base.ts ================================================ import { ChatParam, ProviderType } from "../../types"; import { ModelChatResult } from "../provider"; export class AbstractModelProvider { config: { type: ProviderType; url: string; apiUrl: string; apiHost: string; apiKey: string; [key: string]: any; }; constructor(config: { type: ProviderType; url: string; apiUrl: string; apiHost: string; apiKey: string; [key: string]: any; }) { this.config = config; } async chat(prompt: string, chatParam: ChatParam): Promise { return Promise.reject(new Error("Method not implemented.")); } } ================================================ FILE: src/module/Model/provider/driver/openai.ts ================================================ import { ModelChatResult } from "../provider"; import { ChatParam, ProviderType } from "../../types"; import { AbstractModelProvider } from "./base"; export class OpenAiModelProvider extends AbstractModelProvider { constructor(config: { type: ProviderType; url: string; apiUrl: string; apiHost: string; apiKey: string; [p: string]: any; }) { super(config); } async chat(prompt: string, chatParam: ChatParam): Promise { // this.config.url = 'http://localhost:3000/v1/chat/completions'; // this.config.apiKey = ''; chatParam = Object.assign( { systemPrompt: null, }, chatParam, ); const messages: any[] = []; if (chatParam.systemPrompt) { messages.push({ role: "system", content: chatParam.systemPrompt }); } messages.push({ role: "user", content: prompt }); const response = await fetch(this.config.url, { method: "POST", headers: { Authorization: `Bearer ${this.config.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: this.config.modelId, messages: messages, }), }); if (!response.ok) { const error = await response.text(); throw `Request failed: ${response.status}\n${error}`; } // check if is json if ( !response.headers.get("content-type")?.includes("application/json") ) { const error = await response.text(); throw `Response is not json: ${response.status}\n${error}`; } const data = await response.json(); try { const content = data.choices[0].message.content; return { code: 0, msg: "ok", data: { content, }, }; } catch (e) { throw `Invalid response format: ${JSON.stringify(data)}`; } } } ================================================ FILE: src/module/Model/provider/provider.ts ================================================ import { ChatParam, ProviderType } from "../types"; import { OpenAiModelProvider } from "./driver/openai"; import { mapError } from "../../../lib/error"; const ModelProviderMap = { openai: OpenAiModelProvider, }; export type ModelChatResult = { code: number; msg: string; data?: { content?: string; [key: string]: any; }; }; export const ModelProvider = { apiUrl(type: ProviderType, apiUrl: string, apiHost: string = "") { let url = apiUrl; if (apiHost) { url = apiHost; } // console.log('ModelProvider.apiUrl', {type, apiUrl, apiHost, url}); switch (type) { case "openai": /** * 根据传入的 url 判断是否需要在其末尾加 `/v[数字]/`。 * - 如果 以 `/` 结尾,则不加 * - 要加:其余情况。 */ if (url.endsWith("/")) { return `${url}chat/completions`; } if (url.endsWith("/chat/completions")) { return url; } return `${url}/v1/chat/completions`; } throw new Error(`Unsupported provider type: ${type}`); }, async chat( prompt: string, chatParam: ChatParam, config: { type: ProviderType; modelId: string; apiUrl: string; apiHost: string; apiKey: string; }, ): Promise { let url = this.apiUrl(config.type, config.apiUrl, config.apiHost); if (!(config.type in ModelProviderMap)) { return { code: -1, msg: `Unsupported provider type: ${config.type}`, }; } const provider = new ModelProviderMap[config.type]({ ...config, url, }); try { return provider.chat(prompt, chatParam); } catch (e) { return { code: -1, msg: `Request failed: ${mapError(e)}`, }; } }, }; ================================================ FILE: src/module/Model/providers.ts ================================================ import { t } from "../../lang"; import BuildInProviderLogo from "./../../assets/image/logo.svg"; import ZhinaoProviderLogo from "./assets/image/models/360.png"; import HunyuanProviderLogo from "./assets/image/models/hunyuan.png"; import AzureProviderLogo from "./assets/image/models/microsoft.png"; import AiHubMixProviderLogo from "./assets/image/providers/aihubmix.jpg"; import AlayaNewProviderLogo from "./assets/image/providers/alayanew.webp"; import AnthropicProviderLogo from "./assets/image/providers/anthropic.png"; import BaichuanProviderLogo from "./assets/image/providers/baichuan.png"; import BaiduCloudProviderLogo from "./assets/image/providers/baidu-cloud.svg"; import BailianProviderLogo from "./assets/image/providers/bailian.png"; import DeepSeekProviderLogo from "./assets/image/providers/deepseek.png"; import DmxapiProviderLogo from "./assets/image/providers/DMXAPI.png"; import FireworksProviderLogo from "./assets/image/providers/fireworks.png"; import GiteeAIProviderLogo from "./assets/image/providers/gitee-ai.png"; import GithubProviderLogo from "./assets/image/providers/github.png"; import GoogleProviderLogo from "./assets/image/providers/google.png"; import GPUStackProviderLogo from "./assets/image/providers/gpustack.svg"; import GraphRagProviderLogo from "./assets/image/providers/graph-rag.png"; import GrokProviderLogo from "./assets/image/providers/grok.png"; import GroqProviderLogo from "./assets/image/providers/groq.png"; import HyperbolicProviderLogo from "./assets/image/providers/hyperbolic.png"; import InfiniProviderLogo from "./assets/image/providers/infini.png"; import JinaProviderLogo from "./assets/image/providers/jina.png"; import LMStudioProviderLogo from "./assets/image/providers/lmstudio.png"; import MinimaxProviderLogo from "./assets/image/providers/minimax.png"; import MistralProviderLogo from "./assets/image/providers/mistral.png"; import ModelScopeProviderLogo from "./assets/image/providers/modelscope.png"; import MoonshotProviderLogo from "./assets/image/providers/moonshot.png"; import NvidiaProviderLogo from "./assets/image/providers/nvidia.png"; import O3ProviderLogo from "./assets/image/providers/o3.png"; import OcoolAiProviderLogo from "./assets/image/providers/ocoolai.png"; import OllamaProviderLogo from "./assets/image/providers/ollama.png"; import OpenAiProviderLogo from "./assets/image/providers/openai.png"; import OpenRouterProviderLogo from "./assets/image/providers/openrouter.png"; import PerplexityProviderLogo from "./assets/image/providers/perplexity.png"; import PPIOProviderLogo from "./assets/image/providers/ppio.png"; import SiliconFlowProviderLogo from "./assets/image/providers/silicon.png"; import StepProviderLogo from "./assets/image/providers/step.png"; import TencentCloudProviderLogo from "./assets/image/providers/tencent-cloud-ti.png"; import TogetherProviderLogo from "./assets/image/providers/together.png"; import BytedanceProviderLogo from "./assets/image/providers/volcengine.png"; import VoyageAIProviderLogo from "./assets/image/providers/voyageai.png"; import XirangProviderLogo from "./assets/image/providers/xirang.png"; import ZeroOneProviderLogo from "./assets/image/providers/zero-one.png"; import ZhipuProviderLogo from "./assets/image/providers/zhipu.png"; import { ModelProvider } from "./provider/provider"; import { Provider } from "./types"; const ProviderLogoMap = { buildIn: BuildInProviderLogo, openai: OpenAiProviderLogo, silicon: SiliconFlowProviderLogo, deepseek: DeepSeekProviderLogo, "gitee-ai": GiteeAIProviderLogo, yi: ZeroOneProviderLogo, groq: GroqProviderLogo, zhipu: ZhipuProviderLogo, ollama: OllamaProviderLogo, lmstudio: LMStudioProviderLogo, moonshot: MoonshotProviderLogo, openrouter: OpenRouterProviderLogo, baichuan: BaichuanProviderLogo, dashscope: BailianProviderLogo, modelscope: ModelScopeProviderLogo, xirang: XirangProviderLogo, anthropic: AnthropicProviderLogo, aihubmix: AiHubMixProviderLogo, gemini: GoogleProviderLogo, stepfun: StepProviderLogo, doubao: BytedanceProviderLogo, "graphrag-kylin-mountain": GraphRagProviderLogo, minimax: MinimaxProviderLogo, github: GithubProviderLogo, copilot: GithubProviderLogo, ocoolai: OcoolAiProviderLogo, together: TogetherProviderLogo, fireworks: FireworksProviderLogo, zhinao: ZhinaoProviderLogo, nvidia: NvidiaProviderLogo, "azure-openai": AzureProviderLogo, hunyuan: HunyuanProviderLogo, grok: GrokProviderLogo, hyperbolic: HyperbolicProviderLogo, mistral: MistralProviderLogo, jina: JinaProviderLogo, ppio: PPIOProviderLogo, "baidu-cloud": BaiduCloudProviderLogo, dmxapi: DmxapiProviderLogo, perplexity: PerplexityProviderLogo, infini: InfiniProviderLogo, o3: O3ProviderLogo, "tencent-cloud-ti": TencentCloudProviderLogo, gpustack: GPUStackProviderLogo, alayanew: AlayaNewProviderLogo, voyageai: VoyageAIProviderLogo, } as const; export function getProviderLogo(providerId: string) { return ProviderLogoMap[providerId as keyof typeof ProviderLogoMap]; } export function getProviderUrl(provider: Provider) { return ModelProvider.apiUrl( provider.type, provider.apiUrl, provider.data.apiHost, ); } export const getProviderTitle = (providerId: string) => { const map = { buildIn: "provider.buildIn", aihubmix: "AiHubMix", alayanew: "Alaya NeW", anthropic: "Anthropic", "azure-openai": "Azure OpenAI", baichuan: "provider.baichuan", "baidu-cloud": "provider.baiduCloud", copilot: "GitHub Copilot", dashscope: "provider.dashscope", deepseek: "provider.deepseek", dmxapi: "DMXAPI", doubao: "provider.doubao", fireworks: "Fireworks", gemini: "Gemini", "gitee-ai": "Gitee AI", github: "GitHub Models", gpustack: "GPUStack", "graphrag-kylin-mountain": "GraphRAG", grok: "Grok", groq: "Groq", hunyuan: "provider.hunyuan", hyperbolic: "Hyperbolic", infini: "provider.infini", jina: "Jina", lmstudio: "LM Studio", minimax: "MiniMax", mistral: "Mistral", modelscope: "provider.modelscope", moonshot: "provider.moonshot", nvidia: "provider.nvidia", o3: "O3", ocoolai: "ocoolAI", ollama: "Ollama", openai: "OpenAI", openrouter: "OpenRouter", perplexity: "Perplexity", ppio: "provider.ppio", qwenlm: "QwenLM", silicon: "provider.silicon", stepfun: "provider.stepfun", "tencent-cloud-ti": "provider.tencentCloudTi", together: "Together", xirang: "provider.xirang", yi: "provider.yi", zhinao: "provider.zhinao", zhipu: "provider.zhipu", voyageai: "Voyage AI", }; const key = map[providerId as keyof typeof map]; if (!key) return providerId; // Non-i18n entries (plain strings, not keys) start with capital/non-dot pattern if (!key.includes(".")) return key; return t(key) || key; }; export const SystemProviders = { openai: { api: { url: "https://api.openai.com", }, websites: { official: "https://openai.com/", apiKey: "https://platform.openai.com/api-keys", docs: "https://platform.openai.com/docs", models: "https://platform.openai.com/docs/models", }, }, o3: { api: { url: "https://api.o3.fan", }, websites: { official: "https://o3.fan", apiKey: "https://o3.fan/token", docs: "https://docs.o3.fan", models: "https://docs.o3.fan/models", }, }, ppio: { api: { url: "https://api.ppinfra.com/v3/openai", }, websites: { official: "https://ppinfra.com/model-api/product/llm-api?utm_source=github_cherry-studio&utm_medium=github_readme&utm_campaign=link", apiKey: "https://ppinfra.com/settings/key-management", docs: "https://ppinfra.com/docs/model-api/reference/llm/llm.html", models: "https://ppinfra.com/model-api/product/llm-api?utm_source=github_cherry-studio&utm_medium=github_readme&utm_campaign=link", }, }, gemini: { api: { url: "https://generativelanguage.googleapis.com", }, websites: { official: "https://gemini.google.com/", apiKey: "https://aistudio.google.com/app/apikey", docs: "https://ai.google.dev/gemini-api/docs", models: "https://ai.google.dev/gemini-api/docs/models/gemini", }, }, silicon: { api: { url: "https://api.siliconflow.cn", }, websites: { official: "https://www.siliconflow.cn/", apiKey: "https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88", docs: "https://docs.siliconflow.cn/", models: "https://docs.siliconflow.cn/docs/model-names", }, }, "gitee-ai": { api: { url: "https://ai.gitee.com", }, websites: { official: "https://ai.gitee.com/", apiKey: "https://ai.gitee.com/dashboard/settings/tokens", docs: "https://ai.gitee.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90/POST/chat/completions", models: "https://ai.gitee.com/serverless-api", }, }, deepseek: { api: { url: "https://api.deepseek.com", }, websites: { official: "https://deepseek.com/", apiKey: "https://platform.deepseek.com/api_keys", docs: "https://platform.deepseek.com/api-docs/", models: "https://platform.deepseek.com/api-docs/", }, }, ocoolai: { api: { url: "https://api.ocoolai.com", }, websites: { official: "https://one.ocoolai.com/", apiKey: "https://one.ocoolai.com/token", docs: "https://docs.ocoolai.com/", models: "https://api.ocoolai.com/info/models/", }, }, together: { api: { url: "https://api.together.xyz", }, websites: { official: "https://www.together.ai/", apiKey: "https://api.together.ai/settings/api-keys", docs: "https://docs.together.ai/docs/introduction", models: "https://docs.together.ai/docs/chat-models", }, }, dmxapi: { api: { url: "https://www.dmxapi.cn", }, websites: { official: "https://www.dmxapi.cn/register?aff=bwwY", apiKey: "https://www.dmxapi.cn/register?aff=bwwY", docs: "https://dmxapi.cn/models.html#code-block", models: "https://www.dmxapi.cn/pricing", }, }, perplexity: { api: { url: "https://api.perplexity.ai/", }, websites: { official: "https://perplexity.ai/", apiKey: "https://www.perplexity.ai/settings/api", docs: "https://docs.perplexity.ai/home", models: "https://docs.perplexity.ai/guides/model-cards", }, }, infini: { api: { url: "https://cloud.infini-ai.com/maas", }, websites: { official: "https://cloud.infini-ai.com/", apiKey: "https://cloud.infini-ai.com/iam/secret/key", docs: "https://docs.infini-ai.com/gen-studio/api/maas.html#/operations/chatCompletions", models: "https://cloud.infini-ai.com/genstudio/model", }, }, github: { api: { url: "https://models.inference.ai.azure.com/", }, websites: { official: "https://github.com/marketplace/models", apiKey: "https://github.com/settings/tokens", docs: "https://docs.github.com/en/github-models", models: "https://github.com/marketplace/models", }, }, copilot: { api: { url: "https://api.githubcopilot.com/", }, }, yi: { api: { url: "https://api.lingyiwanwu.com", }, websites: { official: "https://platform.lingyiwanwu.com/", apiKey: "https://platform.lingyiwanwu.com/apikeys", docs: "https://platform.lingyiwanwu.com/docs", models: "https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B", }, }, zhipu: { api: { url: "https://open.bigmodel.cn/api/paas/v4/", }, websites: { official: "https://open.bigmodel.cn/", apiKey: "https://open.bigmodel.cn/usercenter/apikeys", docs: "https://open.bigmodel.cn/dev/howuse/introduction", models: "https://open.bigmodel.cn/modelcenter/square", }, }, moonshot: { api: { url: "https://api.moonshot.cn", }, websites: { official: "https://moonshot.ai/", apiKey: "https://platform.moonshot.cn/console/api-keys", docs: "https://platform.moonshot.cn/docs/", models: "https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8", }, }, baichuan: { api: { url: "https://api.baichuan-ai.com", }, websites: { official: "https://www.baichuan-ai.com/", apiKey: "https://platform.baichuan-ai.com/console/apikey", docs: "https://platform.baichuan-ai.com/docs", models: "https://platform.baichuan-ai.com/price", }, }, modelscope: { api: { url: "https://api-inference.modelscope.cn/v1/", }, websites: { official: "https://modelscope.cn", apiKey: "https://modelscope.cn/my/myaccesstoken", docs: "https://modelscope.cn/docs/model-service/API-Inference/intro", models: "https://modelscope.cn/models", }, }, xirang: { api: { url: "https://wishub-x1.ctyun.cn", }, websites: { official: "https://www.ctyun.cn", apiKey: "https://huiju.ctyun.cn/service/serviceGroup", docs: "https://www.ctyun.cn/products/ctxirang", models: "https://huiju.ctyun.cn/modelSquare/", }, }, dashscope: { api: { url: "https://dashscope.aliyuncs.com/compatible-mode/v1/", }, websites: { official: "https://www.aliyun.com/product/bailian", apiKey: "https://bailian.console.aliyun.com/?apiKey=1#/api-key", docs: "https://help.aliyun.com/zh/model-studio/getting-started/", models: "https://bailian.console.aliyun.com/model-market#/model-market", }, }, stepfun: { api: { url: "https://api.stepfun.com", }, websites: { official: "https://platform.stepfun.com/", apiKey: "https://platform.stepfun.com/interface-key", docs: "https://platform.stepfun.com/docs/overview/concept", models: "https://platform.stepfun.com/docs/llm/text", }, }, doubao: { api: { url: "https://ark.cn-beijing.volces.com/api/v3/", }, websites: { official: "https://console.volcengine.com/ark/", apiKey: "https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=DB4II4FC", docs: "https://www.volcengine.com/docs/82379/1182403", models: "https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint", }, }, minimax: { api: { url: "https://api.minimax.chat/v1/", }, websites: { official: "https://platform.minimaxi.com/", apiKey: "https://platform.minimaxi.com/user-center/basic-information/interface-key", docs: "https://platform.minimaxi.com/document/Announcement", models: "https://platform.minimaxi.com/document/Models", }, }, alayanew: { api: { url: "https://deepseek.alayanew.com", }, websites: { official: "https://www.alayanew.com/backend/register?id=cherrystudio", apiKey: " https://www.alayanew.com/backend/register?id=cherrystudio", docs: "https://docs.alayanew.com/docs/modelService/interview?utm_source=cherrystudio", models: "https://www.alayanew.com/product/deepseek?id=cherrystudio", }, }, openrouter: { api: { url: "https://openrouter.ai/api/v1/", }, websites: { official: "https://openrouter.ai/", apiKey: "https://openrouter.ai/settings/keys", docs: "https://openrouter.ai/docs/quick-start", models: "https://openrouter.ai/docs/models", }, }, groq: { api: { url: "https://api.groq.com/openai", }, websites: { official: "https://groq.com/", apiKey: "https://console.groq.com/keys", docs: "https://console.groq.com/docs/quickstart", models: "https://console.groq.com/docs/models", }, }, ollama: { api: { url: "http://localhost:11434", }, websites: { official: "https://ollama.com/", docs: "https://github.com/ollama/ollama/tree/main/docs", models: "https://ollama.com/library", }, }, lmstudio: { api: { url: "http://localhost:1234", }, websites: { official: "https://lmstudio.ai/", docs: "https://lmstudio.ai/docs", models: "https://lmstudio.ai/models", }, }, anthropic: { api: { url: "https://api.anthropic.com/", }, websites: { official: "https://anthropic.com/", apiKey: "https://console.anthropic.com/settings/keys", docs: "https://docs.anthropic.com/en/docs", models: "https://docs.anthropic.com/en/docs/about-claude/models", }, }, grok: { api: { url: "https://api.x.ai", }, websites: { official: "https://x.ai/", docs: "https://docs.x.ai/", models: "https://docs.x.ai/docs#getting-started", }, }, hyperbolic: { api: { url: "https://api.hyperbolic.xyz", }, websites: { official: "https://app.hyperbolic.xyz", apiKey: "https://app.hyperbolic.xyz/settings", docs: "https://docs.hyperbolic.xyz", models: "https://app.hyperbolic.xyz/models", }, }, mistral: { api: { url: "https://api.mistral.ai", }, websites: { official: "https://mistral.ai", apiKey: "https://console.mistral.ai/api-keys/", docs: "https://docs.mistral.ai", models: "https://docs.mistral.ai/getting-started/models/models_overview", }, }, jina: { api: { url: "https://api.jina.ai", }, websites: { official: "https://jina.ai", apiKey: "https://jina.ai/", docs: "https://jina.ai", models: "https://jina.ai", }, }, aihubmix: { api: { url: "https://aihubmix.com", }, websites: { official: "https://aihubmix.com?aff=SJyh", apiKey: "https://aihubmix.com?aff=SJyh", docs: "https://doc.aihubmix.com/", models: "https://aihubmix.com/models", }, }, fireworks: { api: { url: "https://api.fireworks.ai/inference", }, websites: { official: "https://fireworks.ai/", apiKey: "https://fireworks.ai/account/api-keys", docs: "https://docs.fireworks.ai/getting-started/introduction", models: "https://fireworks.ai/dashboard/models", }, }, zhinao: { api: { url: "https://api.360.cn", }, websites: { official: "https://ai.360.com/", apiKey: "https://ai.360.com/platform/keys", docs: "https://ai.360.com/platform/docs/overview", models: "https://ai.360.com/platform/limit", }, }, hunyuan: { api: { url: "https://api.hunyuan.cloud.tencent.com", }, websites: { official: "https://cloud.tencent.com/product/hunyuan", apiKey: "https://console.cloud.tencent.com/hunyuan/api-key", docs: "https://cloud.tencent.com/document/product/1729/111007", models: "https://cloud.tencent.com/document/product/1729/104753", }, }, nvidia: { api: { url: "https://integrate.api.nvidia.com", }, websites: { official: "https://build.nvidia.com/explore/discover", apiKey: "https://build.nvidia.com/meta/llama-3_1-405b-instruct", docs: "https://docs.api.nvidia.com/nim/reference/llm-apis", models: "https://build.nvidia.com/nim", }, }, "azure-openai": { api: { url: "", }, websites: { official: "https://azure.microsoft.com/en-us/products/ai-services/openai-service", apiKey: "https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/OpenAI", docs: "https://learn.microsoft.com/en-us/azure/ai-services/openai/", models: "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models", }, }, "baidu-cloud": { api: { url: "https://qianfan.baidubce.com/v2/", }, websites: { official: "https://cloud.baidu.com/", apiKey: "https://console.bce.baidu.com/iam/#/iam/apikey/list", docs: "https://cloud.baidu.com/doc/index.html", models: "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu", }, }, "tencent-cloud-ti": { api: { url: "https://api.lkeap.cloud.tencent.com", }, websites: { official: "https://cloud.tencent.com/product/ti", apiKey: "https://console.cloud.tencent.com/lkeap/api", docs: "https://cloud.tencent.com/document/product/1772", models: "https://console.cloud.tencent.com/tione/v2/aimarket", }, }, gpustack: { api: { url: "", }, websites: { official: "https://gpustack.ai/", docs: "https://docs.gpustack.ai/latest/", models: "https://docs.gpustack.ai/latest/overview/#supported-models", }, }, voyageai: { api: { url: "https://api.voyageai.com", }, websites: { official: "https://www.voyageai.com/", apiKey: "https://dashboard.voyageai.com/organization/api-keys", docs: "https://docs.voyageai.com/docs", models: "https://docs.voyageai.com/docs", }, }, }; ================================================ FILE: src/module/Model/store/model.ts ================================================ import { defineStore } from "pinia"; import store from "../../../store/index"; import { debounce } from "lodash-es"; import { watch } from "vue"; import { AppConfig } from "../../../config"; import { t } from "../../../lang"; import { Dialog } from "../../../lib/dialog"; import { mapError } from "../../../lib/error"; import { ObjectUtil, StringUtil } from "../../../lib/util"; import { useUserStore } from "../../../store/modules/user"; import { SystemModels } from "../models"; import { ModelChatResult, ModelProvider } from "../provider/provider"; import { getProviderLogo, getProviderTitle, SystemProviders, } from "../providers"; import { ChatParam, Model, Provider } from "../types"; const userStore = useUserStore(); export type ModelItem = { id: string; providerId: string; providerLogo: string; providerTitle: string; modelId: string; modelName: string; }; watch( () => userStore.data, (newValue) => { model.init().then(); }, { deep: true, }, ); const mapModelError = (e: unknown, provider: Provider) => { if (provider.id === "buildIn") { const msg = String(e); const showCharge = () => { $mapi.user .open({ readyParam: { page: "ChargeLmApi", }, }) .then(); }; const map = { insufficient_user_quota: { msg: t("error.energyInsufficient"), callback: showCharge, }, }; for (const key in map) { if (msg.includes(key)) { const error = map[key]; if (error.callback) { setTimeout(() => { error.callback(); }, 3000); } return error.msg; } } } return mapError(e); }; export const modelStore = defineStore("model", { state() { return { providers: [] as Provider[], }; }, actions: { async init() { const results: Provider[] = []; for (const providerId in SystemProviders) { const provider = SystemProviders[providerId]; results.push({ id: providerId, type: "openai", title: getProviderTitle(providerId), logo: getProviderLogo(providerId), isSystem: true, apiUrl: provider.api.url, websites: { official: provider.websites?.official, docs: provider.websites?.docs, models: provider.websites?.models, }, data: { apiKey: "", apiHost: "", models: (SystemModels[providerId] || []).map((m) => { return { id: m.id, provider: providerId, name: m.name, group: m.group, types: ["text" as Model["types"][number]], enabled: false, editable: false, } satisfies Model; }), enabled: false, }, }); } let buildInProviderData: Partial | null = null; const storageData = await $mapi.storage.read("models"); if (storageData) { if (storageData.userProviders) { storageData.userProviders.forEach((provider) => { results.unshift({ id: provider.id, type: provider.type, title: provider.title, logo: null, isSystem: false, apiUrl: "", websites: { official: "", docs: "", models: "", }, data: { apiKey: "", apiHost: "", models: [], enabled: false, }, }); }); } if (storageData.providerData) { buildInProviderData = storageData.providerData["buildIn"] || null; for (const providerId in storageData.providerData) { const provider = results.find( (p) => p.id === providerId, ); if (provider) { provider.data.apiKey = storageData.providerData[providerId].apiKey || ""; provider.data.apiHost = storageData.providerData[providerId].apiHost; ( storageData.providerData[providerId].models || [] ).forEach((model) => { const existingModel = provider.data.models.find( (m) => m.id === model.id, ); if (existingModel) { existingModel.name = model.name; existingModel.group = model.group; existingModel.types = model.types; existingModel.enabled = model.enabled || false; } else { provider.data.models.push({ id: model.id, provider: providerId, name: model.name, group: model.group, types: ["text"], enabled: model.enabled || false, editable: true, }); } }); provider.data.enabled = storageData.providerData[providerId].enabled || false; } } } } this.providers = results; await this.refreshBuildIn(buildInProviderData); }, async enabledModels(): Promise { const results: ModelItem[] = []; this.providers.forEach((provider) => { if (provider.data.enabled) { provider.data.models.forEach((model) => { if (model.enabled) { results.push({ id: provider.id + "|" + model.id, providerId: provider.id, providerLogo: provider.logo || "", providerTitle: provider.title || "", modelId: model.id, modelName: model.name, }); } }); } }); return results; }, async refreshBuildIn( buildInProviderData?: Partial | null, ) { if ( userStore.data && userStore.data.lmApi && userStore.data.lmApi.models ) { const lmApi = userStore.data.lmApi; const buildInProvider = this.providers.find( (p) => p.id === "buildIn", ); if (!buildInProvider) { const models: Model[] = []; for (const m of lmApi.models) { models.push({ id: m, provider: "buildIn", name: m, group: "Default", types: ["text"], enabled: true, editable: false, }); } // console.log("model.init.buildIn", JSON.stringify({lmApi}, null, 2)); let enabled = true; if ( buildInProviderData && "enabled" in buildInProviderData ) { enabled = buildInProviderData.enabled ?? true; } console.log("model.init.buildIn", { enabled, buildInProviderData, }); this.providers.unshift({ id: "buildIn", type: "openai", title: getProviderTitle("buildIn"), logo: getProviderLogo("buildIn"), isSystem: true, apiUrl: lmApi.apiUrl, websites: { official: AppConfig.website, docs: AppConfig.website, models: AppConfig.website, }, data: { apiKey: lmApi.apiKey, apiHost: "", models: models, enabled: enabled, }, }); } else { buildInProvider.data.apiKey = lmApi.apiKey; } } }, async add(provider: Partial) { const p: Provider = { id: provider.id || StringUtil.random(8), type: provider.type || "openai", title: provider.title || "", logo: null, isSystem: false, apiUrl: "", websites: { official: "", docs: "", models: "", }, data: { apiKey: "", apiHost: "", models: [], enabled: false, }, }; this.providers.unshift(p); await this.sync(); }, async edit(provider: Partial) { const p = this.providers.find((p) => p.id === provider.id); if (p) { if ("title" in provider) { p.title = provider.title || ""; } if ("type" in provider) { p.type = provider.type || "openai"; } if (provider.data) { if ("apiKey" in provider.data) { p.data.apiKey = provider.data.apiKey; } if ("apiHost" in provider.data) { p.data.apiHost = provider.data.apiHost; } if ("enabled" in provider.data) { p.data.enabled = provider.data.enabled; } } await this.sync(); } }, async test(providerId: string, modelId: string) { await this.refreshBuildIn(); const provider = this.providers.find((p) => p.id === providerId); if (!provider) { return; } const m = provider.data.models.find((m) => m.id === modelId); if (!m) { return; } Dialog.loadingOn(t("common.testing")); try { const ret = await ModelProvider.chat( t("model.testPrompt"), { systemPrompt: null, }, { type: provider.type, modelId: m.id, apiUrl: provider.apiUrl, apiHost: provider.data.apiHost, apiKey: provider.data.apiKey, }, ); if (ret.code) { throw ret.msg; } Dialog.tipSuccess(t("common.testSuccess")); } catch (e) { Dialog.tipError( t("common.testFailed") + " " + mapModelError(e, provider), ); } finally { Dialog.loadingOff(); } }, async chat( providerId: string, modelId: string, prompt: string, chatParam: ChatParam, option?: { loading: boolean; }, ): Promise { await this.refreshBuildIn(); if (!providerId || !modelId) { Dialog.tipError(t("hint.selectModel")); return { code: -1, msg: t("hint.selectModel") }; } option = Object.assign( { loading: false, }, option, ); const provider = this.providers.find((p) => p.id === providerId); // console.log("provider.chat", JSON.stringify({provider}, null, 2)); if (!provider) { return { code: -1, msg: "provider not found" }; } const m = provider.data.models.find((m) => m.id === modelId); if (!m) { return { code: -1, msg: "model not found" }; } if (option.loading) { Dialog.loadingOn(); } try { return await ModelProvider.chat(prompt, chatParam, { type: provider.type, modelId: m.id, apiUrl: provider.apiUrl, apiHost: provider.data.apiHost, apiKey: provider.data.apiKey, }); } catch (e) { return { code: -1, msg: mapModelError(e, provider) }; } finally { if (option.loading) { Dialog.loadingOff(); } } }, async change( providerId: string, key: "" | "data.apiKey" | "data.apiHost" | "data.enabled", value: string | boolean, ) { const provider = model.providers.find((p) => p.id === providerId); if (!provider) { return; } const keys = key.split("."); let obj = provider; for (let i = 0; i < keys.length - 1; i++) { obj = obj[keys[i]]; } const lastKey = keys[keys.length - 1]; if (obj && lastKey in obj) { obj[lastKey] = value; } await this.sync(); }, async modelAdd(providerId: string, model: Partial) { const provider = this.providers.find((p) => p.id === providerId); if (!provider) { return; } const m: Model = { id: model.id || StringUtil.random(8), provider: providerId, name: model.name || "", group: model.group || "", types: model.types || ["text"], enabled: true, editable: model.editable ?? true, }; provider.data.models.unshift(m); await this.sync(); }, async modelDelete(providerId: string, modelId: string) { const provider = this.providers.find((p) => p.id === providerId); if (!provider) { return; } const m = provider.data.models.find((m) => m.id === modelId); if (m) { provider.data.models.splice(provider.data.models.indexOf(m), 1); } await this.sync(); }, async modelEdit(providerId: string, model: Partial) { const provider = this.providers.find((p) => p.id === providerId); if (!provider) { return; } const m = provider.data.models.find((m) => m.id === model.id); if (m) { if ("name" in model) { m.name = model.name || ""; } if ("group" in model) { m.group = model.group || ""; } if ("types" in model) { m.types = model.types || ["text"]; } if ("enabled" in model) { m.enabled = model.enabled as boolean; } } await this.sync(); }, async changeModel( providerId: string, modelId: string, key: "enabled", value: boolean, ) { const provider = this.providers.find((p) => p.id === providerId); if (!provider) { return; } const m = provider.data.models.find((m) => m.id === modelId); if (m) { m[key] = value; } await this.sync(); }, sync: debounce(async () => { const providerData = {}; model.providers.forEach((provider) => { providerData[provider.id] = ObjectUtil.clone(provider.data); if (provider.id === "buildIn") { providerData[provider.id].apiKey = ""; } }); const userProviders = model.providers.filter( (provider) => !provider.isSystem, ); await $mapi.storage.write( "models", ObjectUtil.clone({ providerData, userProviders }), ); }, 200), }, }); export const model = modelStore(store); model.init().then(); export const useModelStore = () => { return model; }; ================================================ FILE: src/module/Model/types.ts ================================================ export type ProviderType = "openai"; // | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai' export type ModelType = "text"; // | 'vision' | 'embedding' | 'reasoning' | 'function_calling' export type Model = { id: string; provider: string; name: string; group: string; types: ModelType[]; enabled: boolean; editable: boolean; }; export type Provider = { id: string; type: ProviderType; logo: string | null; title: string; isSystem: boolean; apiUrl: string; websites: { official: string; docs: string; models: string; }; data: { apiKey: string; apiHost: string; models: Model[]; enabled: boolean; }; runtime?: {}; }; export type ChatParam = { systemPrompt: string | null; }; ================================================ FILE: src/pages/DetachWindow/operate.ts ================================================ import { Menu } from "@electron/remote"; import { t } from "../../lang"; export const useDetachWindowOperate = ({ plugin }) => { const doShowZoomMenu = () => { const menuTemplate: any[] = []; const zoomPercent = [ 50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300, ]; for (let z of zoomPercent) { menuTemplate.push({ label: `${z}%`, click: async () => { await window.$mapi.manager.setDetachPluginZoom(z); plugin.value.runtime.config.zoom = z; }, }); } Menu.buildFromTemplate(menuTemplate).popup(); }; const doShowMoreMenu = () => { const autoDetach = !!plugin.value.runtime.config.autoDetach; const menuTemplate: any[] = []; menuTemplate.push({ label: t("plugin.debugWindow"), click: async () => { await window.$mapi.manager.openDetachPluginDevTools(); }, }); menuTemplate.push({ label: t("plugin.backendLog"), click: async () => { await window.$mapi.manager.openDetachPluginLog(); }, }); if (!(plugin.value.setting && plugin.value.setting.autoDetach)) { menuTemplate.push({ label: t("plugin.autoDetachWindow"), type: "checkbox", checked: autoDetach, click: async () => { await window.$mapi.manager.setPluginAutoDetach(!autoDetach); plugin.value.runtime.config = await window.$mapi.manager.getPluginConfig( plugin.value.name, ); }, }); } if (plugin.value.setting) { if ( plugin.value.setting.moreMenu && plugin.value.setting.moreMenu.length > 0 ) { for (const item of plugin.value.setting.moreMenu) { ((item) => { menuTemplate.push({ label: item.title, click: async () => { await window.$mapi.manager.firePluginMoreMenuClick( item.name, ); }, }); })(item); } } } Menu.buildFromTemplate(menuTemplate).popup(); }; const doClose = async () => { await window.$mapi.manager.closeDetachPlugin(); }; return { doShowZoomMenu, doShowMoreMenu, doClose, }; }; ================================================ FILE: src/pages/FastPanel/FastPanelResult.vue ================================================ ================================================ FILE: src/pages/FastPanel/FastPanelSearch.vue ================================================ ================================================ FILE: src/pages/FastPanel/Lib/resultOperate.ts ================================================ import { useManagerStore } from "../../../store/modules/manager"; import { ActionRecord } from "../../../types/Manager"; const manager = useManagerStore(); export const useResultOperate = () => { const doOpenAction = async (action: ActionRecord) => { // await manager.showMainWindow() await manager.openAction(action); }; return { doOpenAction, }; }; ================================================ FILE: src/pages/Home.vue ================================================ ================================================ FILE: src/pages/Main/Components/ResultActionCodeError.vue ================================================ ================================================ FILE: src/pages/Main/Components/ResultActionCodeItemList.vue ================================================ ================================================ FILE: src/pages/Main/Components/ResultActionCodeLoading.vue ================================================ ================================================ FILE: src/pages/Main/Components/ResultItem.vue ================================================ ================================================ FILE: src/pages/Main/Components/ResultLoading.vue ================================================ ================================================ FILE: src/pages/Main/Components/ResultWindowItem.vue ================================================ ================================================ FILE: src/pages/Main/Lib/entryListener.ts ================================================ import { useManagerStore } from "../../../store/modules/manager"; import { ClipboardDataType, SelectedContent } from "../../../types/Manager"; import { TimeUtil } from "../../../lib/util"; const manager = useManagerStore(); export const EntryListener = { prepareSearch: async (option: { // 主动粘贴 isPaste?: boolean; // 快速面板 isFastPanel?: boolean; }) => { // console.log('EntryListener.prepareSearch', option) option = Object.assign( { isPaste: false, isFastPanel: false, }, option, ); // console.log('EntryListener.prepareSearch', option) let searchValue = manager.searchValue; let selectedContent: SelectedContent | null = null; // the fast panel should check the selected content if (option.isFastPanel) { selectedContent = await window.$mapi.manager.getSelectedContent(); } const clipboardContent: ClipboardDataType | null = await window.$mapi.manager.getClipboardContent(); let useClipboard = false; // first use clipboard if (manager.showFirstRun) { manager.showFirstRun = false; const clipboardChangeTime = await window.$mapi.manager.getClipboardChangeTime(); // only use clipboard if it has changed in the last 3 seconds if ( clipboardChangeTime > 0 && clipboardChangeTime > TimeUtil.timestamp() - 3 ) { useClipboard = true; } } if (!useClipboard && option.isPaste) { useClipboard = true; } // files manager.setCurrentFiles([]); if ( selectedContent && selectedContent.type === "file" && selectedContent.files?.length ) { manager.setCurrentFiles(selectedContent.files as FileItem[]); } else if ( useClipboard && clipboardContent && clipboardContent.type === "file" && clipboardContent.files?.length ) { manager.setCurrentFiles(clipboardContent.files as FileItem[]); } // image manager.setCurrentImage(""); if ( useClipboard && clipboardContent && clipboardContent.type === "image" && clipboardContent.image ) { manager.setCurrentImage(clipboardContent.image); } // text manager.setCurrentText(""); if ( selectedContent && selectedContent.type === "text" && selectedContent.text ) { manager.setCurrentText(selectedContent.text); } else if ( useClipboard && clipboardContent && clipboardContent.type === "text" && clipboardContent.text ) { manager.setCurrentText(clipboardContent.text); } if (!option.isFastPanel && manager.currentText) { if ( manager.currentText.split("\n").length === 1 && manager.currentText.length < 100 ) { if ( !manager.searchLastKeywords || (manager.searchLastKeywords && manager.searchLastKeywords !== manager.currentText) ) { searchValue = manager.currentText; manager.setCurrentText(""); } } } if (option.isFastPanel) { await manager.searchFastPanel(searchValue); } else { await manager.search(searchValue); } // console.log('state', JSON.stringify({ // searchValue, // option, // useClipboard, // clipboardContent, // image: manager.currentImage, // files: manager.currentFiles, // text: manager.currentText // }, null, 2)) }, }; ================================================ FILE: src/pages/Main/Lib/mainOperate.ts ================================================ import { useManagerStore } from "../../../store/modules/manager"; import { computed } from "vue"; const manager = useManagerStore(); export const useMainOperate = () => { const hasActions = computed(() => { return ( manager.searchActions.length > 0 || manager.matchActions.length > 0 || manager.historyActions.length > 0 || manager.pinActions.length > 0 ); }); let detachHotKey: any = null; let detachHotkeyExpire = 0; let detachHotkeyTimes = 0; const onKeyDown = (e: KeyboardEvent) => { let resultKey = ""; const { ctrlKey, shiftKey, altKey, metaKey } = e; const modifiers: Array = []; ctrlKey && modifiers.push("control"); shiftKey && modifiers.push("shift"); altKey && modifiers.push("alt"); metaKey && modifiers.push("meta"); if (!detachHotKey) { detachHotKey = manager.configGet("detachWindowTrigger", null); } // console.log('keydown', e) // {"key":"D","altKey":false,"ctrlKey":false,"metaKey":true,"shiftKey":false,"times":1} if (detachHotKey && detachHotKey.value) { // console.log('detachHotkeyExpire', detachHotKey.value.key, detachHotkeyExpire) if ( detachHotKey.value.key === e.key.toUpperCase() && detachHotKey.value.altKey === altKey && detachHotKey.value.ctrlKey === ctrlKey && detachHotKey.value.metaKey === metaKey && detachHotKey.value.shiftKey === shiftKey ) { if (!detachHotkeyExpire || Date.now() > detachHotkeyExpire) { detachHotkeyExpire = Date.now() + 500; detachHotkeyTimes = 1; } else { detachHotkeyTimes++; } if (detachHotkeyTimes >= detachHotKey.value.times) { detachHotkeyExpire = 0; detachHotkeyTimes = 0; manager.detachPlugin(); return { resultKey, }; } } } const map = { Escape: "esc", ArrowLeft: "left", ArrowRight: "right", ArrowDown: "down", ArrowUp: "up", Enter: "enter", " ": "space", }; const key = map[e.key] || "custom"; switch (key) { case "up": case "down": case "left": case "right": case "enter": if (hasActions.value) { resultKey = key; } break; case "esc": if (manager.activePlugin) { manager.closeMainPlugin().then(); } else { manager.hideMainWindow().then(); } break; default: switch (e.keyCode) { case 8: if (manager.searchValue === "") { resultKey = "delete"; } break; case 86: if (manager.searchValue === "") { if (ctrlKey || metaKey) { resultKey = "paste"; } } break; } break; } if (resultKey) { e.preventDefault(); } return { resultKey, }; }; return { onKeyDown, }; }; ================================================ FILE: src/pages/Main/Lib/resultOperate.ts ================================================ import { ComputedRef } from "@vue/reactivity"; import { chunk } from "lodash-es"; import { computed, ref, watch } from "vue"; import { t } from "../../../lang"; import { Dialog } from "../../../lib/dialog"; import { UI } from "../../../lib/ui"; import { useManagerStore } from "../../../store/modules/manager"; import { ActionRecord } from "../../../types/Manager"; import { EntryListener } from "./entryListener"; type ActionGroupType = | "window" | "search" | "match" | "history" | "pin" | never; const manager = useManagerStore(); export const useResultOperate = () => { const hasActions = computed(() => { return ( manager.detachWindowActions.length > 0 || manager.searchActions.length > 0 || manager.matchActions.length > 0 || manager.historyActions.length > 0 || manager.pinActions.length > 0 ); }); const hasViewActions = computed(() => { return manager.viewActions.length > 0; }); const lineActionCount = computed(() => { return manager.viewActions.length > 0 ? 5 : 8; }); const searchActionIsExtend = ref(false); const matchActionIsExtend = ref(false); const historyActionIsExtend = ref(false); const pinActionIsExtend = ref(false); watch( () => manager.searchActions, () => { searchActionIsExtend.value = manager.searchActions.length <= lineActionCount.value; resetActive(); }, ); watch( () => manager.matchActions, () => { matchActionIsExtend.value = manager.matchActions.length <= lineActionCount.value; resetActive(); }, ); watch( () => manager.historyActions, () => { historyActionIsExtend.value = manager.historyActions.length <= lineActionCount.value; resetActive(); }, ); watch( () => manager.pinActions, () => { pinActionIsExtend.value = manager.pinActions.length <= lineActionCount.value; resetActive(); }, ); const doSearchActionExtend = () => { if (searchActionIsExtend.value) { return; } searchActionIsExtend.value = true; }; const doMatchActionExtend = () => { if (matchActionIsExtend.value) { return; } matchActionIsExtend.value = true; }; const doHistoryActionExtend = () => { if (historyActionIsExtend.value) { return; } historyActionIsExtend.value = true; }; const doPinActionExtend = () => { if (pinActionIsExtend.value) { return; } pinActionIsExtend.value = true; }; const showDetachWindowActions: ComputedRef = computed( () => { return manager.detachWindowActions; }, ); const showSearchActions: ComputedRef = computed(() => { return searchActionIsExtend.value ? manager.searchActions : manager.searchActions.slice(0, lineActionCount.value); }); const showMatchActions: ComputedRef = computed(() => { return matchActionIsExtend.value ? manager.matchActions : manager.matchActions.slice(0, lineActionCount.value); }); const showHistoryActions: ComputedRef = computed(() => { return historyActionIsExtend.value ? manager.historyActions : manager.historyActions.slice(0, lineActionCount.value); }); const showPinActions: ComputedRef = computed(() => { return pinActionIsExtend.value ? manager.pinActions : manager.pinActions.slice(0, lineActionCount.value); }); const activeActionGroup = ref("search"); const actionActionIndex = ref(0); const resetActive = () => { if (manager.detachWindowActions.length > 0) { activeActionGroup.value = "window"; } else if (manager.searchActions.length > 0) { activeActionGroup.value = "search"; } else if (manager.matchActions.length > 0) { activeActionGroup.value = "match"; } else if (manager.historyActions.length > 0) { activeActionGroup.value = "history"; } else if (manager.pinActions.length > 0) { activeActionGroup.value = "pin"; } actionActionIndex.value = 0; }; const doCodeNavigate = (direction: string) => { let index = manager.actionCodeItems.findIndex( (item) => item.id === manager.actionCodeItemActiveId, ); switch (direction) { case "up": case "left": index = Math.max(index - 1, 0); break; case "down": case "right": index = Math.min(index + 1, manager.actionCodeItems.length - 1); break; } manager.actionCodeItemActiveId = manager.actionCodeItems[index].id; setTimeout(() => { const codeItemElement = document.getElementById( `MainResult_CodeItem_${manager.actionCodeItemActiveId}`, ); if (codeItemElement) { const container = document.getElementById( "MainResult_Container", ); if (container) { UI.smoothScrollTop( container, codeItemElement.offsetTop - container.offsetTop - container.clientHeight / 2 + codeItemElement.clientHeight / 2, ).then(() => { // 计算完全在可视范围内的元素,使用shortcutIndex进行编号 const visibleItemIndexes = Array.from( container.querySelectorAll( ".pb-main-result-code-item", ), ) .map((el, idx) => { const item = el as HTMLElement; const itemTop = item.offsetTop - container.offsetTop; const itemBottom = itemTop + item.clientHeight; if ( itemTop >= container.scrollTop && itemBottom <= container.scrollTop + container.clientHeight ) { return idx; } return null; }) .filter((idx) => idx !== null) as number[]; manager.actionCodeItems.forEach((item, idx) => { if (visibleItemIndexes.includes(idx)) { item.shortcutIndex = visibleItemIndexes.indexOf(idx) + 1; } else { item.shortcutIndex = -1; } }); }); } } }, 10); }; const _doActionNavigate = (direction: string) => { const grids: any[][] = []; [ [showDetachWindowActions.value, "window"], [showSearchActions.value, "search"], [showMatchActions.value, "match"], [showHistoryActions.value, "history"], [showPinActions.value, "pin"], ].forEach((actions) => { let items = [] as any[]; (actions[0] as ActionRecord[]).forEach((_, itemIndex) => { items.push({ group: actions[1], index: itemIndex, }); }); chunk(items, lineActionCount.value).forEach((chunk) => { grids.push(chunk); }); }); let activeGridRowIndex = grids.findIndex((gridLine) => gridLine.find( (grid) => grid.group === activeActionGroup.value && grid.index === actionActionIndex.value, ), ); let activeGridColIndex = grids[activeGridRowIndex].findIndex( (grid) => grid.group === activeActionGroup.value && grid.index === actionActionIndex.value, ); switch (direction) { case "up": if (activeGridRowIndex > 0) { activeGridRowIndex--; activeGridColIndex = Math.min( activeGridColIndex, grids[activeGridRowIndex].length - 1, ); } break; case "down": if (activeGridRowIndex < grids.length - 1) { activeGridRowIndex++; activeGridColIndex = Math.min( activeGridColIndex, grids[activeGridRowIndex].length - 1, ); } break; case "left": activeGridColIndex--; if (activeGridColIndex < 0) { if (activeGridRowIndex > 0) { activeGridRowIndex--; activeGridColIndex = grids[activeGridRowIndex].length - 1; } else { activeGridColIndex = 0; } } break; case "right": activeGridColIndex++; if (activeGridColIndex >= grids[activeGridRowIndex].length) { if (activeGridRowIndex < grids.length - 1) { activeGridRowIndex++; activeGridColIndex = 0; } else { activeGridColIndex = grids[activeGridRowIndex].length - 1; } } break; } activeActionGroup.value = grids[activeGridRowIndex][activeGridColIndex].group; actionActionIndex.value = grids[activeGridRowIndex][activeGridColIndex].index; manager.setSelectedAction(_getActiveAction() as ActionRecord); }; const _getActiveAction = () => { let activeAction: any = null; switch (activeActionGroup.value) { case "window": activeAction = showDetachWindowActions.value[actionActionIndex.value]; break; case "search": activeAction = showSearchActions.value[actionActionIndex.value]; break; case "match": activeAction = showMatchActions.value[actionActionIndex.value]; break; case "history": activeAction = showHistoryActions.value[actionActionIndex.value]; break; case "pin": activeAction = showPinActions.value[actionActionIndex.value]; break; } return activeAction as ActionRecord | null; }; const onInputKey = (key: string) => { if (["up", "down", "left", "right"].includes(key)) { if (manager.activePlugin && manager.activePluginType === "code") { doCodeNavigate(key); } else { _doActionNavigate(key); } } else if ("enter" === key) { if (manager.activePlugin && manager.activePluginType === "code") { if (manager.actionCodeItemActiveId) { doOpenActionCode(manager.actionCodeItemActiveId).then(); } return; } if (manager.searchIsCompositing) { return; } const action = _getActiveAction(); if (action) { if (activeActionGroup.value === "window") { openActionWindow("open", action).then(); } else { doOpenAction(action).then(); } } } else if ("delete" === key) { if ("" === manager.searchValue) { onClose(); } } else if ("paste" === key) { if (!manager.activePlugin) { EntryListener.prepareSearch({ isPaste: true }).then(); } } }; const onClose = () => { if (manager.activePlugin) { doClosePlugin().then(); } else { manager.setCurrentFiles([]); manager.setCurrentImage(""); manager.setCurrentText(""); manager.search("").then(); } }; const doClosePlugin = async () => { await manager.closeMainPlugin(); }; const doOpenAction = async (action: ActionRecord) => { await manager.openAction(action); }; const doOpenActionCode = async (id: string) => { await manager.openActionCode(id); }; const openActionWindow = async (type: "open", action: ActionRecord) => { await manager.openActionWindow(type, action); }; const doHistoryClear = async () => { Dialog.confirm(t("main.clearAllConfirm")).then(() => { window.$mapi.manager.historyClear(); manager.searchRefresh().then(); }); }; const doHistoryDelete = async (action: ActionRecord) => { await window.$mapi.manager.historyDelete( action.pluginName as string, action.name, ); await manager.searchRefresh(); }; const doPinToggle = async (action: ActionRecord) => { await window.$mapi.manager.togglePinAction( action.pluginName as string, action.name, ); await manager.searchRefresh(); }; return { hasActions, hasViewActions, searchActionIsExtend, matchActionIsExtend, historyActionIsExtend, pinActionIsExtend, doSearchActionExtend, doMatchActionExtend, doHistoryActionExtend, doPinActionExtend, showDetachWindowActions, showSearchActions, showMatchActions, showHistoryActions, showPinActions, activeActionGroup, actionActionIndex, onInputKey, onClose, doOpenAction, doOpenActionCode, openActionWindow, doHistoryClear, doHistoryDelete, doPinToggle, }; }; ================================================ FILE: src/pages/Main/Lib/resultResize.ts ================================================ import { onBeforeUnmount, onMounted } from "vue"; import { UI } from "../../../lib/ui"; import { useManagerStore } from "../../../store/modules/manager"; import { WindowConfig } from "../../../../electron/config/window"; const manager = useManagerStore(); let ignoreNextResize = false; export const ignoreNextResultResize = () => { ignoreNextResize = true; }; export const useResultResize = (groupContainer: any) => { onMounted(() => { UI.onResize(groupContainer.value, (width: number, height: number) => { // console.log('resize', width, height, manager.activePlugin) if (!manager.activePlugin && !ignoreNextResize) { manager.resize(width, height + WindowConfig.mainHeight).then(); } else if ( manager.activePlugin && manager.activePluginType === "code" ) { manager.resize(width, height + WindowConfig.mainHeight).then(); } }); }); onBeforeUnmount(() => { UI.offResize(groupContainer.value); }); }; export const fireResultResize = (groupContainer: any) => { // console.log('fireResultResize', groupContainer.value) UI.fireResize(groupContainer.value); }; ================================================ FILE: src/pages/Main/Lib/searchOperate.ts ================================================ import { computed } from "vue"; import { t } from "../../../lang"; import { useManagerStore } from "../../../store/modules/manager"; const { Menu } = require("@electron/remote"); const manager = useManagerStore(); export const useSearchOperate = (emit) => { const doShowMenu = () => { const menuTemplate: any[] = []; if (manager.activePlugin) { if (manager.activePluginType === "code") { // do nothing } else { menuTemplate.push({ label: t("plugin.detachWindow"), click: () => { doDetachPlugin().then(); }, }); } menuTemplate.push({ label: t("plugin.debugWindow"), click: async () => { manager.openMainPluginDevTools().then(); }, }); menuTemplate.push({ label: t("plugin.backendLog"), click: async () => { manager.openMainPluginLog().then(); }, }); if (manager.activePlugin.setting) { if ( manager.activePlugin.setting.moreMenu && manager.activePlugin.setting.moreMenu.length > 0 ) { for (const item of manager.activePlugin.setting.moreMenu) { ((item) => { menuTemplate.push({ label: item.title, click: async () => { await window.$mapi.manager.firePluginMoreMenuClick( item.name, ); }, }); })(item); } } } } if (!menuTemplate.length) { return; } Menu.buildFromTemplate(menuTemplate).popup(); }; const doDetachPlugin = async () => { await manager.detachPlugin(); }; const clipboardFilesInfo = computed<{ name: string; extName: string; }>(() => { const result = { name: t("main.multipleFiles"), extName: "ext.unknown", }; if (manager.currentFiles.length <= 0) { return result; } // 只有一个文件的情况 if (manager.currentFiles.length === 1) { const file = manager.currentFiles[0]; result.name = file.name; result.extName = file.name; if (file.isDirectory) { result.extName = "ext.folder"; } if (result.name.endsWith(".fad")) { result.name = result.name.substring(0, result.name.length - 4); } return result; } // 如果全部是目录 const directoryCount = manager.currentFiles.filter( (f) => f.isDirectory, ).length; if (directoryCount === manager.currentFiles.length) { result.name = t("main.multipleFolders"); result.extName = "ext.folder"; return result; } // 如果全部是文件 const fileCount = manager.currentFiles.filter((f) => f.isFile).length; if (fileCount === manager.currentFiles.length) { // 如果全部是图片 const imageCount = manager.currentFiles.filter((f) => { const ext = f.name.split(".").pop()?.toLowerCase(); return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes( ext || "", ); }).length; if (imageCount === manager.currentFiles.length) { result.name = t("main.multipleImages"); result.extName = "ext.png"; return result; } } return result; }); const onSearchDoubleClick = () => { if (manager.activePlugin) { doDetachPlugin().then(); } }; return { onSearchDoubleClick, doShowMenu, clipboardFilesInfo, }; }; ================================================ FILE: src/pages/Main/Lib/viewOperate.ts ================================================ import { ActionTypeEnum, PluginType } from "../../../types/Manager"; import { computed, watch } from "vue"; import { useManagerStore } from "../../../store/modules/manager"; import { useSettingStore } from "../../../store/modules/setting"; const executePluginHooks = async (web: any, hook: string, data?: any) => { const evalJs = ` if(window.focusany && window.focusany.hooks && typeof window.focusany.hooks.on${hook} === 'function' ) { try { window.focusany.hooks.on${hook}(${JSON.stringify(data)}); } catch(e) { console.log('executePluginHooks.on${hook}.error', e); } }`; return web.executeJavaScript(evalJs); }; const manager = useManagerStore(); const setting = useSettingStore(); export const useViewOperate = (type: "fastPanel" | "main") => { const webUserAgent = window.$mapi.app.getUserAgent(); const viewActions = computed(() => { if (type === "main") { return manager.viewActions.map((a) => { a["_web"] = null; a["_webInit"] = false; a["_webReady"] = false; a["_height"] = a.runtime?.view?.heightView || 100; return a; }); } return manager.fastPanelViewActions.map((a) => { a["_web"] = null; a["_webInit"] = false; a["_webReady"] = false; a["_height"] = a.runtime?.view?.heightView || 100; return a; }); }); const queryWeb = () => { // console.log('queryWeb.entry', viewActions.value.map(a => a['_web'])) for (const a of viewActions.value) { if (a.type !== ActionTypeEnum.VIEW) { continue; } if (!a["_web"] || a["_webInit"]) { continue; } a["_webInit"] = true; // console.log('queryWeb', a['_web']) const readyData = {}; readyData["actionName"] = a.name; readyData["actionMatch"] = a.runtime?.match; readyData["actionMatchFiles"] = a.runtime?.matchFiles; readyData["requestId"] = a.runtime?.requestId as any; readyData["reenter"] = false; readyData["isView"] = true; ((aa) => { aa["_web"].addEventListener("did-finish-load", async () => { aa["_webReady"] = true; aa["_web"].insertCSS(`body{overflow: hidden;}`); if (setting.shouldDarkMode()) { aa["_web"].executeJavaScript(` document.body.setAttribute('data-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark'); `); if (aa.pluginType === PluginType.SYSTEM) { aa["_web"].executeJavaScript( `document.body.setAttribute('arco-theme', 'dark');`, ); } } }); aa["_web"].addEventListener("dom-ready", async () => { await executePluginHooks( a["_web"], "PluginReady", readyData, ); if (aa.runtime?.view?.showViewDevTools) { aa["_web"].openDevTools({ mode: "detach", activate: false, }); } }); aa["_web"].addEventListener("ipc-message", (event) => { if ("FocusAny.View" === event.channel) { const { id, type, data } = event.args[0]; switch (type) { case "view.setHeight": aa["_height"] = data.height; break; case "view.getHeight": // console.log('view.getHeight', aa['_height']) aa["_web"].send( `FocusAny.View.${id}`, aa["_height"], ); break; } } }); })(a); } }; watch( () => viewActions.value, () => { queryWeb(); }, { deep: true, }, ); return { webUserAgent, viewActions, }; }; ================================================ FILE: src/pages/Main/MainResult.vue ================================================ ================================================ FILE: src/pages/Main/MainSearch.vue ================================================ ================================================ FILE: src/pages/PageAbout.vue ================================================ ================================================ FILE: src/pages/PageDetachWindow.vue ================================================ ================================================ FILE: src/pages/PageFastPanel.vue ================================================ ================================================ FILE: src/pages/PageFeedback.vue ================================================ ================================================ FILE: src/pages/PageGuide.vue ================================================ ================================================ FILE: src/pages/PageLog.vue ================================================ ================================================ FILE: src/pages/PageMonitor.vue ================================================ ================================================ FILE: src/pages/PagePayment.vue ================================================ ================================================ FILE: src/pages/PageSetup.vue ================================================ ================================================ FILE: src/pages/PageStore.vue ================================================ ================================================ FILE: src/pages/PageSystem.vue ================================================ ================================================ FILE: src/pages/PageUser.vue ================================================ ================================================ FILE: src/pages/PageWorkflow.vue ================================================ ================================================ FILE: src/pages/Setting.vue ================================================ ================================================ FILE: src/pages/System/SystemAbout.vue ================================================ ================================================ FILE: src/pages/System/SystemAction.vue ================================================ ================================================ FILE: src/pages/System/SystemData.vue ================================================ ================================================ FILE: src/pages/System/SystemFile.vue ================================================ ================================================ FILE: src/pages/System/SystemLaunch.vue ================================================ ================================================ FILE: src/pages/System/SystemMCP.vue ================================================ ================================================ FILE: src/pages/System/SystemModel.vue ================================================ ================================================ FILE: src/pages/System/SystemPlugin.vue ================================================ ================================================ FILE: src/pages/System/SystemSetting.vue ================================================ ================================================ FILE: src/pages/System/SystemUser.vue ================================================ ================================================ FILE: src/pages/System/components/ActionTypeIcon.vue ================================================ ================================================ FILE: src/pages/System/components/HotkeyInput.vue ================================================ ================================================ FILE: src/pages/System/components/SystemActionMatchDetailDialog.vue ================================================ ================================================ FILE: src/pages/System/components/SystemDataBackup/WebDavManage.vue ================================================ ================================================ FILE: src/pages/System/components/SystemDataBackup/WebDavManageSettingDialog.vue ================================================ ================================================ FILE: src/pages/System/components/SystemDataBackupDialog.vue ================================================ ================================================ FILE: src/pages/System/components/SystemDataViewDetailDialog.vue ================================================ ================================================ FILE: src/pages/System/components/SystemDataViewDialog.vue ================================================ ================================================ FILE: src/pages/System/components/type.ts ================================================ import { PluginRecord } from "../../../types/Manager"; export type SystemDataRecord = { plugin: PluginRecord; count: 0; }; ================================================ FILE: src/router.ts ================================================ import { createRouter, createWebHashHistory } from "vue-router"; const routes = [ { path: "/", component: () => import("./layouts/Main.vue"), children: [ { path: "", component: () => import("./pages/Home.vue") }, { path: "setting", component: () => import("./pages/Setting.vue") }, ], }, { path: "/", component: () => import("./layouts/Raw.vue"), children: [], }, ]; const router = createRouter({ history: createWebHashHistory(), routes, }); // watch router change router.beforeEach((to, from, next) => { window.$mapi?.statistics?.tick("visit", { path: to.path, }); next(); }); export default router; ================================================ FILE: src/store/index.ts ================================================ import { createPinia } from "pinia"; const store = createPinia(); export default store; ================================================ FILE: src/store/modules/app.ts ================================================ import { defineStore } from "pinia"; import store from "../index"; export const appStore = defineStore("app", { state() { return {}; }, actions: { async init() {}, }, }); export const app = appStore(store); app.init().then(() => {}); export const useAppStore = () => { return app; }; ================================================ FILE: src/store/modules/manager.ts ================================================ import debounce from "lodash/debounce"; import { defineStore } from "pinia"; import { computed, toRaw } from "vue"; import { WindowConfig } from "../../../electron/config/window"; import { t } from "../../lang"; import { ActionRecord, ActionTypeEnum, ConfigRecord, PluginRecord, } from "../../types/Manager"; import store from "../index"; const searchFastPanelActionDebounce = debounce((query, cb) => { window.$mapi.manager.searchFastPanelAction(query).then((result) => { cb(result); }); }); const searchDebounce = debounce((query, cb) => { window.$mapi.manager.searchAction(query).then((result) => { cb(result); }); }, 300); const subInputChangeDebounce = debounce((keywords) => { window.$mapi.manager.subInputChange(keywords); }, 300); const searchActionCodeDebounce = debounce((keywords) => { window.$mapi.manager.searchActionCode(keywords).then(); }, 300); export const managerStore = defineStore("manager", { state: () => ({ config: {} as ConfigRecord, showFirstRun: false, searchLoading: false, searchLastKeywords: "", searchValue: "", searchPlaceholder: t("main.placeholder"), searchSubPlaceholder: "", searchSubIsVisible: false, searchIsCompositing: false, detachWindowActions: [] as ActionRecord[], searchActions: [] as ActionRecord[], matchActions: [] as ActionRecord[], historyActions: [] as ActionRecord[], pinActions: [] as ActionRecord[], viewActions: [] as ActionRecord[], selectedAction: null as ActionRecord | null, activePlugin: null as PluginRecord | null, activePluginType: null as "code" | null, activePluginLoading: false, actionCodeLoading: false, actionCodeError: null as string | null, actionCodeType: null as "list" | null, actionCodeItemActiveId: null as string | null, actionCodeItems: [] as { id: string; shortcutIndex: number; [key: string]: any; }[], currentFiles: [] as FileItem[], currentImage: "", currentText: "", fastPanelActionLoading: false, fastPanelMatchActions: [] as ActionRecord[], fastPanelViewActions: [] as ActionRecord[], notice: null as { text: string; type: "info" | "error" | "success"; duration: number; } | null, noticeCleanTimer: null as any, }), actions: { async init() { this.config = await window.$mapi.manager.getConfig(); }, async setConfig(key: string, value: any) { // console.log('setConfig', key, value, toRaw(this.config)) this.config[key] = value; await window.$mapi.manager.setConfig(toRaw(this.config)); }, async onConfigChange(key: string, value: any) { return await this.setConfig(key, toRaw(value)); }, configGet(key: string, defaultValue: any = null) { return computed(() => { if (key in this.config) { return this.config[key]; } return defaultValue; }); }, setActivePluginLoading(loading: boolean) { this.activePluginLoading = loading; }, setActivePlugin( plugin: PluginRecord | null, type: "code" | null = null, ) { this.activePlugin = plugin; this.activePluginType = plugin ? type : null; }, setSearchValue(value: string) { if (this.activePlugin) { return; } this.searchValue = value; }, setSelectedAction(action: ActionRecord) { this.selectedAction = action; document .querySelector(`[data-action="${action.fullName}"]`) ?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center", }); }, setCurrentFiles(files: FileItem[]) { this.currentFiles = files; }, setCurrentImage(image: string) { this.currentImage = image; }, setCurrentText(text: string) { this.currentText = text; }, async searchFastPanel(keywords: string) { this.fastPanelMatchActions = []; this.fastPanelViewActions = []; this.fastPanelActionLoading = true; searchFastPanelActionDebounce( { keywords: keywords, currentFiles: toRaw(this.currentFiles), currentImage: this.currentImage, currentText: this.currentText, }, (result: { matchActions: ActionRecord[]; viewActions: ActionRecord[]; }) => { this.fastPanelMatchActions = result.matchActions; this.fastPanelViewActions = result.viewActions; this.fastPanelActionLoading = false; }, ); }, async searchRefresh() { await this.search(this.searchLastKeywords); }, async search(keywords: string) { if (this.activePlugin) { if (this.activePluginType === "code") { this.searchValue = keywords; searchActionCodeDebounce(keywords); return; } subInputChangeDebounce(keywords); this.searchValue = keywords; return; } this.searchLoading = true; this.searchValue = keywords; this.viewActions = []; searchDebounce( { keywords, currentFiles: toRaw(this.currentFiles), currentImage: this.currentImage, currentText: this.currentText, }, (result: { detachWindowActions: ActionRecord[]; searchActions: ActionRecord[]; matchActions: ActionRecord[]; viewActions: ActionRecord[]; historyActions: ActionRecord[]; pinActions: ActionRecord[]; }) => { this.searchLastKeywords = keywords; this.detachWindowActions = result.detachWindowActions; this.searchActions = result.searchActions; this.matchActions = result.matchActions; this.viewActions = result.viewActions; this.historyActions = result.historyActions; this.pinActions = result.pinActions; this.searchLoading = false; }, ); }, async detachWindowActionsRefresh() { this.detachWindowActions = await window.$mapi.manager.listDetachWindowActions(); }, async resize(width: number, height: number) { height = Math.min(height, WindowConfig.mainMaxHeight); await window.$mapi.app.windowSetSize( null, WindowConfig.mainWidth, height, { center: false, }, ); }, async isMainWindowShown() { return await window.$mapi.manager.isShown(); }, async showMainWindow() { await window.$mapi.manager.show(); }, async hideMainWindow() { await window.$mapi.manager.hide(); }, async openAction( action: ActionRecord, group: undefined | "window" = undefined, ) { await window.$mapi.manager.openAction(toRaw(action)); if ( action.type === ActionTypeEnum.COMMAND || action.type === ActionTypeEnum.BACKEND ) { await window.$mapi.manager.hide(); } this.searchValue = ""; // this.detachWindowActions = []; // this.searchActions = []; // this.matchActions = []; // this.viewActions = []; // this.historyActions = []; // this.pinActions = []; }, async openActionCode(id: string) { if (!this.activePlugin || this.activePluginType !== "code") { return; } this.actionCodeItemActiveId = id; await window.$mapi.manager.openActionCode(id); }, async openActionWindow(type: "open", action: ActionRecord) { await window.$mapi.manager.openActionWindow(type, toRaw(action)); }, async closeMainPlugin() { await window.$mapi.manager.closeMainPlugin(); }, async openMainPluginDevTools() { await window.$mapi.manager.openMainPluginDevTools(); }, async openMainPluginLog() { await window.$mapi.manager.openMainPluginLog(); }, async detachPlugin() { await window.$mapi.manager.detachPlugin(); }, setSubInput(payload: { placeholder: string; isFocus: boolean; isVisible: boolean; }) { if (!this.activePlugin) { return; } this.searchSubPlaceholder = payload.placeholder || ""; this.searchSubIsVisible = payload.isVisible || false; }, removeSubInput() { if (!this.activePlugin) { return; } this.searchSubPlaceholder = ""; this.searchSubIsVisible = false; this.searchValue = ""; }, setSubInputValue(value: string) { if (!this.activePlugin) { return; } this.searchValue = value; }, onNotice(data: any) { this.notice = data; if (this.notice?.duration && this.notice?.duration > 0) { if (this.noticeCleanTimer) { clearTimeout(this.noticeCleanTimer); } this.noticeCleanTimer = setTimeout(() => { this.notice = null; }, this.notice.duration); } }, }, }); const manager = managerStore(store); manager.init().then(); window.__page.onBroadcast("Notice", manager.onNotice); export const useManagerStore = () => { return manager; }; ================================================ FILE: src/store/modules/setting.ts ================================================ import { cloneDeep } from "lodash-es"; import { defineStore } from "pinia"; import { computed } from "vue"; import { AppConfig } from "../../config"; import { applyLocale } from "../../lang"; import store from "../index"; export const settingStore = defineStore("setting", { state() { return { version: AppConfig.version, basic: cloneDeep(AppConfig.basic), isDarkMode: false, buildInfo: { buildId: "", }, config: { guideWatched: false as boolean, darkMode: "" as "light" | "dark" | "auto", }, configEnv: {}, }; }, actions: { async init() { this.isDarkMode = await window.$mapi.app.isDarkMode(); this.config = await window.$mapi.config.all(); this.configEnv = await window.$mapi.config.allEnv(); this.setupDarkMode(); this.showGuideWhenReady().then(); window.$mapi.app.getBuildInfo().then((info: any) => { this.buildInfo = info; }); }, async showGuideWhenReady() { if (!(await window.$mapi.app.setupIsOk())) { setTimeout(() => { this.showGuideWhenReady(); }, 1000); return; } setTimeout(() => { if (!this.config.guideWatched) { window.$mapi.app.windowOpen("guide").then(); this.setConfig("guideWatched", true).then(); } }, 2000); }, onConfigChangeBroadcast(data: any) { (async () => { this.config = await window.$mapi.config.all(); this.setupDarkMode(); if (data?.key === "lang" && data?.value) { applyLocale(data.value); } })(); }, onConfigEnvChangeBroadcast(data: any) { (async () => { this.configEnv = await window.$mapi.config.allEnv(); })(); }, onDarkModeChangeBroadcast(data: any) { this.isDarkMode = data.isDarkMode; this.setupDarkMode(); }, shouldDarkMode() { const darkMode = this.config["darkMode"] || "auto"; if ("dark" === darkMode) { return true; } else if ("light" === darkMode) { return false; } else if ("auto" === darkMode) { return this.isDarkMode; } return false; }, setupDarkMode() { // console.log('setupDarkMode') if (this.shouldDarkMode()) { document.body.setAttribute("arco-theme", "dark"); document.body.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark"); } else { document.body.removeAttribute("arco-theme"); document.body.removeAttribute("data-theme"); document.documentElement.removeAttribute("data-theme"); } }, async initBasic(basic: object) { this.basic = Object.assign(this.basic, basic); }, async setConfig(key: string, value: any) { // console.log('setConfig', key, value) this.config[key] = value; await window.$mapi.config.set(key, value); if ("darkMode" === key) { setTimeout(() => this.setupDarkMode(), 100); } }, async setConfigEnv(key: string, value: any) { this.configEnv[key] = value; await window.$mapi.config.setEnv(key, value); }, async onConfigChange(key: string, value: any) { return await this.setConfig(key, value); }, async onConfigEnvChange(key: string, value: any) { return await this.setConfigEnv(key, value); }, configGet(key: string, defaultValue: any = null) { return computed(() => { if (key in this.config) { return this.config[key]; } return defaultValue; }); }, configEnvGet(key: string, defaultValue: any = null) { return computed(() => { if (key in this.configEnv) { return this.configEnv[key]; } return defaultValue; }); }, }, }); const setting = settingStore(store); setting.init().then(); window.__page.onBroadcast("ConfigChange", setting.onConfigChangeBroadcast); window.__page.onBroadcast( "ConfigEnvChange", setting.onConfigEnvChangeBroadcast, ); window.__page.onBroadcast("DarkModeChange", setting.onDarkModeChangeBroadcast); export const useSettingStore = () => { return setting; }; ================================================ FILE: src/store/modules/task.ts ================================================ // ------------------------------------------------------------------------------ // ----------------------------- Task Schedule Store ---------------------------- // ------------------------------------------------------------------------------ // Register TaskBiz // taskStore.register('TestSync', TestSync) // taskStore.register('TestAsync', TestAsync) // Dispatch Task // await taskStore.dispatch('TestSync', StringUtil.random()) // await taskStore.dispatch('TestAsync', StringUtil.random(), {'a':1}, {timeout: 3 * 1000}) // Schedule call order // Sync Task runFunc -> successFunc | failFunc // Async Task runFunc -> queryFunc -> successFunc | failFunc // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ import { cloneDeep } from "lodash-es"; import { defineStore } from "pinia"; import { toRaw } from "vue"; import { mapError } from "../../lib/error"; import { StringUtil, TimeUtil } from "../../lib/util"; import store from "../index"; export type TaskRecordStatus = | "queue" | "running" | "querying" | "success" | "fail" | "delete"; export type TaskRecordRunStatus = "retry" | "success" | "querying"; export type TaskRecordQueryStatus = "running" | "success" | "fail"; export type TaskChangeType = | "running" | "success" | "fail" | "change" | "requestCancel"; export type TaskRecord = { id: string; status: TaskRecordStatus; msg: string; biz: string; bizId: string; bizParam: any; // 开始运行时间 runStart: number; // 是否正在调用 runFunc runCalling: boolean; // 超过 runAfter 才会执行,0表示是一个新任务,>0表示是一个重试任务 runAfter: number; // 是否正在调用 queryFunc queryCalling: boolean; // 超过 queryAfter 才会查询 queryAfter: number; // 查询间隔 queryInterval: number; // 是否正在调用 successFunc successCalling: boolean; // 超时时间 timeout: number; }; export type TaskBiz = { // sync task run, return success | retry // async task run, return querying | success | retry runFunc: (bizId: string, bizParam: any) => Promise; // async task query status, return running | success | fail queryFunc?: ( bizId: string, bizParam: any, ) => Promise; // sync task success callback // async task success callback successFunc: (bizId: string, bizParam: any) => Promise; // Make sure the "failFunc" function always not throw an error failFunc: (bizId: string, msg: string, bizParam: any) => Promise; // request cancel callback, when user request cancel a task, will call this function requestCancelFunc?: (bizId: string, bizParam: any) => Promise; // ---------------------------------------------------- // the following not use in schedule, only for biz [key: string]: any; // ---------------------------------------------------- }; const taskChangeListeners = [] as { biz: string | null; callback: (bizId: string, status: TaskChangeType) => void; }[]; let runNextTimer = null as any; export const TestSync: TaskBiz = { runFunc: async (bizId, bizParam) => { console.log("Task.TestSync.runFunc", { bizId, bizParam }); return "success"; }, successFunc: async (bizId, bizParam) => { console.log("Task.TestSync.successFunc", { bizId, bizParam }); }, failFunc: async (bizId, msg, bizParam) => { console.log("Task.TestSync.failFunc", { bizId, bizParam, msg }); }, }; export const TestAsync: TaskBiz = { runFunc: async (bizId, bizParam) => { console.log("Task.TestAsync.runFunc", { bizId, bizParam }); return "querying"; }, queryFunc(bizId, bizParam) { return new Promise((resolve) => { console.log("Task.TestAsync.queryFunc", { bizId, bizParam }); setTimeout(() => { resolve(Math.random() > 0.7 ? "success" : "running"); }, 1000); }); }, successFunc: async (bizId, bizParam) => { console.log("Task.TestAsync.successFunc", { bizId, bizParam }); }, failFunc: async (bizId, msg, bizParam) => { console.log("Task.TestAsync.failFunc", { bizId, bizParam, msg }); }, }; export const taskStore = defineStore("task", { state() { return { isInit: false, bizMap: {} as Record, records: [] as TaskRecord[], cancelMap: {} as Record< string, { expire: number; } >, }; }, actions: { async init() { await $mapi.storage.get("task", "records", []).then((records) => { this.records = records; this.isInit = true; this._run(true); }); }, async waitInit() { while (!this.isInit) { await new Promise((resolve) => setTimeout(resolve, 100)); } }, _runExecute() { let changed = false; // console.log('task._runExecute.start', JSON.stringify(this.records)) // error record this.records.forEach((record) => { if (!this.bizMap[record.biz]) { record.status = "fail"; record.msg = "biz not found"; changed = true; } }); // console.log('task.records', JSON.stringify(this.records, null, 2)) // request cancel this.records .filter( (r) => r.status === "queue" || r.status === "running" || r.status === "querying", ) .filter((r) => this.shouldCancel(r.biz, r.bizId)) .forEach((record) => { record.status = "fail"; record.msg = mapError("UserCancel"); changed = true; }); // queue this.records .filter((r) => r.status === "queue") .filter((r) => r.runAfter <= Date.now() && !r.runCalling) .forEach((record) => { changed = true; record.status = "running"; record.runStart = Date.now(); record.runCalling = true; let runCallFinish = false; setTimeout(() => { if (runCallFinish) { return; } this.fireChange(record, "running"); }, 1000); this.bizMap[record.biz] .runFunc(record.bizId, record.bizParam) .then((status: TaskRecordRunStatus) => { runCallFinish = true; switch (status) { case "success": record.status = "success"; break; case "querying": record.queryAfter = Date.now() + record.queryInterval; record.status = "querying"; break; case "retry": record.status = "queue"; record.runStart = 0; record.runAfter = Date.now() + 1000; break; } }) .catch((e) => { runCallFinish = true; record.status = "fail"; record.msg = mapError(e); console.error("Task.RunFunc.Error", e); $mapi.log .error("Task.RunFunc.Error", e.toString()) .catch((e) => { console.error("Task.RunFunc.Error.Log", e); }); }) .finally(() => { record.runCalling = false; this.fireChange(record, "running"); }); }); // querying this.records .filter((r) => r.status === "querying") .filter((r) => r.queryAfter <= Date.now() && !r.queryCalling) .forEach((record) => { record.queryCalling = true; const taskBiz = this.bizMap[record.biz]; taskBiz .queryFunc?.(record.bizId, record.bizParam) .then((status: TaskRecordQueryStatus) => { switch (status) { case "running": record.queryAfter = Date.now() + record.queryInterval; break; case "success": record.status = "success"; changed = true; break; case "fail": record.status = "fail"; changed = true; break; } }) .catch((e) => { record.status = "fail"; record.msg = mapError(e); changed = true; console.error("Task.QueryFunc.Error", e); $mapi.log .error("Task.QueryFunc.Error", e.toString()) .catch((e) => { console.error( "Task.QueryFunc.Error.Log", e, ); }); }) .finally(() => { record.queryCalling = false; }); }); // expire this.records .filter( (r) => r.status === "running" || r.status === "querying", ) .filter((r) => Date.now() - r.runStart > r.timeout) .forEach((record) => { record.status = "fail"; record.msg = mapError("ProcessTimeout"); changed = true; }); // success this.records .filter((r) => r.status === "success") .filter((r) => !r.successCalling) .forEach((record) => { record.successCalling = true; changed = true; this.bizMap[record.biz] .successFunc(record.bizId, record.bizParam) .then(() => { record.status = "delete"; }) .catch((e) => { console.error("Task.SuccessFunc.Error", e); $mapi.log .error("Task.SuccessFunc.Error", e.toString()) .catch((e) => { console.error( "Task.SuccessFunc.Error.Log", e, ); }); record.status = "fail"; record.msg = mapError(e); }) .finally(() => { if (record.status === "delete") { this.fireChange(record, "success"); } record.successCalling = false; }); }); // fail this.records .filter((r) => r.status === "fail") .forEach((record) => { changed = true; record.status = "delete"; if (!this.bizMap[record.biz]) { return; } this.bizMap[record.biz] .failFunc(record.bizId, record.msg, record.bizParam) .then(() => {}) .catch((e) => { console.error("Task.FailFunc.Error", e); $mapi.log .error("Task.FailFunc.Error", e.toString()) .catch((e) => { console.error("Task.FailFunc.Error.Log", e); }); }) .finally(() => { this.fireChange(record, "fail"); }); }); // console.log('task._runExecute.end', JSON.stringify(this.records)) // delete this.records = this.records.filter((r) => r.status !== "delete"); // sync if (changed) { this.sync().then(); } // next run // console.log('run', changed, JSON.stringify(this.records)) if (this.records.length > 0) { this._run(changed); } }, _run(immediate: boolean) { if (runNextTimer) { clearTimeout(runNextTimer); runNextTimer = null; } setTimeout( () => { this._runExecute(); }, immediate ? 0 : 1000, ); }, get(biz: string) { return this.bizMap[biz] || null; }, register(biz: string, taskBiz: TaskBiz) { this.bizMap[biz] = taskBiz; }, unregister(biz: string) { delete this.bizMap[biz]; }, onChange( biz: string | null, callback: (bizId: string, type: TaskChangeType) => void, ) { taskChangeListeners.push({ biz, callback }); }, offChange( biz: string | null, callback: (bizId: string, type: TaskChangeType) => void, ) { const index = taskChangeListeners.findIndex( (v) => v.biz === biz && v.callback === callback, ); taskChangeListeners.splice(index, 1); }, fireChange(record: Partial, type: TaskChangeType) { taskChangeListeners.forEach((v) => { if (null === v.biz || v.biz === record.biz) { v.callback(record.bizId as string, type); } }); }, requestCancel(biz: string, bizId: string) { this.cancelMap[`${biz}-${bizId}`] = { expire: TimeUtil.timestampMS() + 60 * 60 * 1000, }; this.fireChange({ biz, bizId }, "requestCancel"); if (this.bizMap[biz]?.requestCancelFunc) { this.bizMap[biz]?.requestCancelFunc?.(bizId, {}).catch((e) => { $mapi.log .error("Task.RequestCancelFunc.Error", e.toString()) .then(); }); } }, shouldCancel(biz: string, bizId: string) { // expire old for (const key in this.cancelMap) { if (this.cancelMap[key].expire < TimeUtil.timestampMS()) { delete this.cancelMap[key]; } } if (!!this.cancelMap[`${biz}-${bizId}`]) { delete this.cancelMap[`${biz}-${bizId}`]; return true; } return false; }, async dispatch( biz: string, bizId: string, bizParam?: any, param?: object, ) { await this.waitInit(); if (!this.bizMap[biz]) { throw new Error("TaskBizNotFound"); } param = Object.assign( { timeout: 24 * 60 * 60 * 1000, queryInterval: 5 * 1000, status: "queue", runStart: 0, }, param, ); const taskRecord = { id: `${biz}-${Date.now()}-${StringUtil.random(8)}`, status: param["status"], msg: "", biz, bizId, bizParam, runStart: param["runStart"], runAfter: 0, runCalling: false, queryAfter: 0, queryInterval: param["queryInterval"], queryCalling: false, successCalling: false, timeout: param["timeout"], } as TaskRecord; this.records.push(taskRecord); this._run(true); }, async sync() { await this.waitInit(); const savedRecords = toRaw(cloneDeep(this.records)); savedRecords.forEach((record) => { // record.status = undefined // record.runtime = undefined }); await $mapi.storage.set("task", "records", savedRecords); }, }, }); export const task = taskStore(store); task.init().then(); export const useTaskStore = () => { return task; }; ================================================ FILE: src/store/modules/user.ts ================================================ import { defineStore } from "pinia"; import store from "../index"; import { AppConfig } from "../../config"; import { useSettingStore } from "./setting"; const setting = useSettingStore(); export const userStore = defineStore("user", { state() { return { isInit: false, lastSavedJson: "", apiToken: null as string | null, user: { id: null as string | null, name: null as string | null, avatar: null as string | null, }, data: { vip: {}, functions: {}, } as { vip: { [key: string]: any; }; functions: { [key: string]: any; }; [key: string]: any; }, basic: {} as { [key: string]: any; }, }; }, actions: { async init() { await this.load(); }, async load() { const { apiToken, user, data, basic } = await window.$mapi.user.get(); this.apiToken = apiToken; this.user = Object.assign(this.user, user); this.data = data as any; this.basic = basic; await setting.initBasic(this.basic); this.isInit = true; }, onChangeBroadcast() { this.load().then(); }, async waitInit() { if (this.isInit) { return; } await new Promise((resolve) => { const timer = setInterval(() => { if (this.isInit) { clearInterval(timer); resolve(undefined); } }, 100); }); }, async webUrl() { await this.waitInit(); let param: string[] = []; if (this.apiToken) { param.push(`api_token=${this.apiToken}`); } if (setting.shouldDarkMode()) { param.push("is_dark=1"); } return `${AppConfig.apiBaseUrl}/app_manager/user_web?${param.join("&")}`; }, }, }); export const user = userStore(store); user.init().then(); window.__page.onBroadcast("UserChange", user.onChangeBroadcast); export const useUserStore = () => { return user; }; ================================================ FILE: src/style.less ================================================ @tailwind base; @tailwind components; @tailwind utilities; :root, body { --primary-1: 255, 255, 255; --primary-2: 207, 207, 207; --primary-3: 160, 160, 160; --primary-4: 112, 112, 112; --primary-5: 65, 65, 65; --primary-6: 17, 17, 17; --primary-7: 17, 13, 13; --primary-8: 17, 9, 9; --primary-9: 17, 4, 6; --primary-10: 17, 0, 2; --color-primary-light-1: rgb(var(--primary-1)); --color-primary-light-2: rgb(var(--primary-2)); --color-primary-light-3: rgb(var(--primary-3)); --color-primary-light-4: rgb(var(--primary-4)); --window-header-height: 2.5rem; --page-nav-width: 4rem; //--color-primary: #5154E0; // 41 85 254 --color-primary: #265BD7; // --color-primary-lighter: #6A6DFF; --color-primary-lighter: lighten(#265BD7, 10%); //--color-bg-page-nav: #2A3A5D; --color-bg-page-nav: #FFFFFF; //--color-bg-page-nav-active: #5154E0; --color-bg-page-nav-active: #FFFFFF; //--color-text-page-nav: #FFFFFF; --color-text-page-nav: #000000; //--color-text-page-nav-active: #EEEEEE; --color-text-page-nav-active: #265BD7; //--color-border-page-nav: #2A3A5D; --color-border-page-nav: #EEEEEE; --color-background: #FFFFFF; --color-background-content: #ededed; --color-text: #111111; --color-border: #E5E6EB; } body[data-theme="dark"] { --primary-1: 17, 0, 2; --primary-2: 17, 4, 6; --primary-3: 17, 9, 9; --primary-4: 17, 13, 13; --primary-5: 17, 17, 17; --primary-6: 65, 65, 65; --primary-7: 112, 112, 112; --primary-8: 160, 160, 160; --primary-9: 207, 207, 207; --primary-10: 255, 255, 255; --color-background: #17171A; --color-background-content: #333333; --color-text: #CCCCCC; --color-border: #484849; --color-text-page-nav: #CCCCCC; --color-bg-page-nav: #17171A; --color-bg-page-nav-active: #2d3443; } body { overflow: hidden; } html { background-color: transparent; } body { background-color: var(--color-background); font-size: 14px; color: var(--color-text); } /////////// layout start /////////// .page-container { height: calc(100vh - var(--window-header-height)); width: 100vw; } .page-narrow-container { max-width: 100rem; margin: 0 auto; } .page-nav-item { color: var(--color-text-page-nav); &.active { background: var(--color-bg-page-nav-active); color: var(--color-text-page-nav-active); } } .window-header { -webkit-user-select: none; .window-header-title { -webkit-app-region: drag; } } * { &::-webkit-scrollbar { width: 6px; height: 6px; } &::-webkit-scrollbar-track { background: #FFFFFF; } &::-webkit-scrollbar-thumb { background: #DDDDDD; border-radius: 10px; } &::-webkit-scrollbar-thumb:hover { background: #CCCCCC; } } body[data-theme="dark"] { * { &::-webkit-scrollbar-track { background: #222222; } &::-webkit-scrollbar-thumb { background: #333333; } &::-webkit-scrollbar-thumb:hover { background: #888888; } } } /////////// layout end /////////// /////////// global start /////////// .text-link { color: #007bff; cursor: pointer; } .hover\:text-primary { &:hover { color: var(--color-primary); } } .bg-default { background-color: var(--color-background); } .bg-primary { background-color: var(--color-primary); } .text-default { color: var(--color-text); } .text-primary { color: var(--color-primary); } .border-default { border-color: var(--color-border); } .plugin-logo-filter { filter: drop-shadow(0 0 1px #FFF); } .debug { border: 1px solid red; } /////////// global end /////////// /////////// arco start /////////// .arco-btn, .arco-input-wrapper, .arco-textarea-wrapper, .arco-input-tag, .arco-radio-group-button, .arco-alert { border-radius: 0.5rem; } .arco-tabs-tab-title { &:before { border-radius: 0.5rem !important; } } .arco-modal-confirm { .arco-modal-header { border-bottom: none; } .arco-modal-footer { border-top: none; } } .arco-select { border-radius: 0.5rem; } .arco-slider { margin-bottom: 0 !important; .arco-slider-mark { font-size: 10px !important; } } .arco-checkbox { padding-left: 0; } .arco-radio-button.arco-radio-checked { border-radius: 0.5rem; } .arco-select-view-multiple { border-radius: 0.5rem; } [data-theme="dark"] { .arco-radio-group-button { .arco-radio-button { color: #CCC; } } } /////////// arco end /////////// ================================================ FILE: src/task/index.ts ================================================ import { useTaskStore } from "../store/modules/task"; import { nextTick } from "vue"; const taskStore = useTaskStore(); export const TaskManager = { init() { // taskStore.register('SoundTts', SoundTts) // taskStore.register('SoundClone', SoundClone) // taskStore.register('VideoGen', VideoGen) nextTick(async () => { // await SoundTts.restore?.() // await SoundClone.restore?.() // await VideoGen.restore?.() }).then(); // taskStore.register('TestSync', TestSync) // taskStore.register('TestAsync', TestAsync) // setInterval(async () => { // // await taskStore.dispatch('TestSync', StringUtil.random()) // await taskStore.dispatch('TestAsync', StringUtil.random(), { // 'a': 1, // }, { // timeout: 3 * 1000, // }) // }, 10 * 1000) }, count() { return taskStore.records.length; }, }; ================================================ FILE: src/types/Manager.ts ================================================ import { HotkeyKeyItem, HotkeyKeySimpleItem, } from "../../electron/mapi/keys/type"; export type ConfigRecord = { mainTrigger: HotkeyKeyItem; detachWindowTrigger: HotkeyKeyItem; fastPanelTrigger: HotkeyKeySimpleItem; }; export type PluginConfig = { autoDetach: boolean; zoom: number; }; export enum PluginType { SYSTEM = "system", STORE = "store", ZIP = "zip", DIR = "dir", } export enum PluginEnv { DEV = "dev", PROD = "prod", } export type PluginPermissionType = "ClipboardManage" | "Api" | "File" | never; export type PluginRecord = { // 以下配置信息和原始的 config.json 一致,未经过处理 name: string; title: string; version: string; logo: string; main: string; mainView?: string; actions: ActionRecord[]; mcp?: { tools?: MCPToolsRecord[]; }; description?: string; preload?: string; platform?: PlatformType[]; versionRequire?: string; editionRequire?: EditionType[]; author?: string; homepage?: string; setting?: { autoDetach?: boolean; detachPosition?: | "center" | "left-top" | "right-top" | "left-bottom" | "right-bottom"; detachAlwaysOnTop?: boolean; width?: string; height?: string; singleton?: boolean; zoom?: number; darkModeSupport?: boolean; httpEntry?: boolean; remoteWebCacheEnable?: boolean; moreMenu?: { name: string; title: string; }[]; preloadBase?: string; nodeIntegration?: boolean; }; permissions?: PluginPermissionType[]; development?: { env?: "dev" | "prod"; main?: string; mainView?: string; showDevTools?: boolean; showCodeDevTools?: boolean; keepCodeDevTools?: boolean; showViewDevTools?: boolean; }; type?: PluginType; env?: PluginEnv; runtime?: { // 插件运行的根目录 root?: string | null; // 配置信息 config?: PluginConfig; // 远程Web信息 remoteWeb?: { userAgent?: string; urlMap?: Record; types?: string[]; domains?: string[]; blocks?: string[]; }; }; }; export type PluginState = { value: string; placeholder: string; isVisible: boolean; }; export type ActionMatch = | ActionMatchText | ActionMatchKey | ActionMatchRegex | ActionMatchFile | ActionMatchImage | ActionMatchWindow | ActionMatchEditor; export enum ActionMatchTypeEnum { TEXT = "text", KEY = "key", REGEX = "regex", IMAGE = "image", FILE = "file", WINDOW = "window", EDITOR = "editor", } export type ActionMatchBase = { type: ActionMatchTypeEnum; name?: string; }; export type ActionMatchText = ActionMatchBase & { text: string; minLength: number; maxLength: number; }; export type ActionMatchKey = ActionMatchBase & { key: string; }; export type ActionMatchRegex = ActionMatchBase & { regex: string; title: string; minLength: number; maxLength: number; }; export type ActionMatchFile = ActionMatchBase & { title: string; minCount: number; maxCount: number; filterFileType: "file" | "directory"; filterExtensions: string[]; }; export type ActionMatchImage = ActionMatchBase & { title: string; }; export type ActionMatchWindow = ActionMatchBase & { nameRegex: string; titleRegex: string; attrRegex: Record; }; export type ActionMatchEditor = ActionMatchBase & { extensions: string[]; fadTypes: string[]; }; export type SelectedContent = { type: "file" | "image" | "text"; files?: FileItem[]; image?: string; text?: string; }; export type ActiveWindow = { name: string; title: string; attr: Record; raw?: any; }; export type ClipboardDataType = { type: "file" | "image" | "text"; files?: FileItem[]; image?: string; text?: string; }; export type ClipboardHistoryRecord = { type: "file" | "image" | "text"; timestamp: number; files?: FileItem[]; image?: string; text?: string; }; export type ActionRecord = { fullName?: string; pluginName?: string; name: string; title: string; matches: ActionMatch[]; pluginType?: PluginType; platform?: PlatformType[]; icon?: string; trackHistory?: boolean; data?: { // type = command command?: string; // type = view showFastPanel?: boolean; showMainPanel?: boolean; }; type?: ActionTypeEnum; runtime?: { searchScore?: number; searchTitleMatched?: string; match?: ActionMatch | null; requestId?: string | null; view?: { nodeIntegration?: boolean; preloadBase?: string; mainView?: string; showViewDevTools?: boolean; heightView?: number; }; matchFiles?: FileItem[]; isPined?: boolean; windowId?: number; windowIndex?: number; windowCount?: number; }; }; export type MCPToolsRecord = { name: string; description: string; inputSchema: { type: "object"; properties: Record< string, { type: string; description?: string; default?: any } >; required?: keyof MCPToolsRecord["inputSchema"]["properties"][]; }; }; export type PluginActionRecord = { pluginName: string; actionName: string; }; export type ActionTypeCodeData = { actionName: string; }; export enum ActionTypeEnum { COMMAND = "command", WEB = "web", CODE = "code", BACKEND = "backend", VIEW = "view", } export type FilePluginRecord = { icon: string; title: string; path: string; }; export type LaunchRecord = { type: "plugin" | "custom"; pluginName: string; name: string; hotkey: HotkeyKeyItem; keyword: string; }; ================================================ FILE: src/vite-env.d.ts ================================================ /// /// import type { Dialog } from "./lib/dialog"; import type { Router } from "vue-router"; declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } declare module "@vue/runtime-core" { interface ComponentCustomProperties { $router: Router; $dialog: Dialog; $t: typeof import("vue-i18n").GlobalTranslate; } } ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue}"], darkMode: ["selector", '[data-theme="dark"]'], theme: { extend: {}, }, plugins: [], }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true, "noImplicitAny": false, "allowJs": true }, "include": ["src", "sdk/focusany.d.ts"], "references": [ { "path": "./tsconfig.node.json" } ] } ================================================ FILE: tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts", "package.json", "electron", "sdk", "src/config.ts", "src/types"] } ================================================ FILE: vite.config.flat.txt ================================================ import { rmSync } from 'node:fs' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import electron from 'vite-plugin-electron' import renderer from 'vite-plugin-electron-renderer' import pkg from './package.json' // https://vitejs.dev/config/ export default defineConfig(({ command }) => { rmSync('dist-electron', { recursive: true, force: true }) const isServe = command === 'serve' const isBuild = command === 'build' const sourcemap = isServe || !!process.env.VSCODE_DEBUG return { plugins: [ vue(), electron([ { // Main process entry file of the Electron App. entry: 'electron/main/index.ts', onstart({ startup }) { if (process.env.VSCODE_DEBUG) { console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') } else { startup() } }, vite: { build: { sourcemap, minify: isBuild, outDir: 'dist-electron/main', rollupOptions: { // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, // we can use `external` to exclude them to ensure they work correctly. // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. // Of course, this is not absolute, just this way is relatively simple. :) external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), }, }, }, }, { entry: 'electron/preload/index.ts', onstart({ reload }) { // Notify the Renderer process to reload the page when the Preload scripts build is complete, // instead of restarting the entire Electron App. reload() }, vite: { build: { sourcemap: sourcemap ? 'inline' : undefined, // #332 minify: isBuild, outDir: 'dist-electron/preload', rollupOptions: { external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), }, }, }, } ]), // Use Node.js API in the Renderer process renderer(), ], server: process.env.VSCODE_DEBUG && (() => { const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) return { host: url.hostname, port: +url.port, } })(), clearScreen: false, } }) ================================================ FILE: vite.config.ts ================================================ import fs from "node:fs"; import {defineConfig} from "vite"; import vue from "@vitejs/plugin-vue"; import Icons from "unplugin-icons/vite"; import electron from "vite-plugin-electron"; import renderer from "vite-plugin-electron-renderer"; import pkg from "./package.json"; import path from "node:path"; import {AppConfig} from "./src/config"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; dayjs.extend(utc); dayjs.extend(timezone); // https://vitejs.dev/config/ export default defineConfig(({command}) => { fs.rmSync("dist-electron", {recursive: true, force: true}); const isServe = command === "serve"; const isBuild = command === "build"; const sourcemap = isServe || !!process.env.VSCODE_DEBUG; const minify = isBuild && !process.env.VSCODE_DEBUG; const externalPackages = [ ...Object.keys("dependencies" in pkg ? pkg.dependencies : {}), ...Object.keys("devDependencies" in pkg ? pkg.devDependencies : {}), ...Object.keys("optionalDependencies" in pkg ? pkg.optionalDependencies : {}), ]; return { plugins: [ vue({ template: { compilerOptions: { isCustomElement: tag => { if (["webview"].includes(tag)) { return true; } return false; }, }, }, }), Icons({ compiler: "vue3", autoInstall: false, }), { name: "add-build-time", generateBundle() { const buildId = dayjs().tz("Asia/Shanghai").format("YYYYMMDDHHmmss"); this.emitFile({ type: "asset", fileName: "build.json", source: JSON.stringify( { buildId, }, null, 2 ), }); }, }, { name: "process-variables", closeBundle() { const files = [ "index.html", "page/about.html", "page/feedback.html", "page/guide.html", "page/monitor.html", "page/payment.html", "page/setup.html", "page/user.html", "page/log.html", ]; files.forEach(f => { const p = path.resolve(__dirname, "dist", f); if(!fs.existsSync(p)) { return; } let html = fs.readFileSync(p, "utf-8"); for (const key in AppConfig) { html = html.replace(new RegExp(`%${key}%`, "g"), AppConfig[key]); } fs.writeFileSync(p, html, "utf-8"); }); }, }, electron([ { // Shortcut of `build.lib.entry` entry: "electron/main/index.ts", onstart({startup}) { if (process.env.VSCODE_DEBUG) { console.log(/* For `.vscode/.debug.script.mjs` */ "[startup] Electron App"); } else { startup(); } }, vite: { build: { sourcemap, minify: minify, outDir: "dist-electron/main", rollupOptions: { // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, // we can use `external` to exclude them to ensure they work correctly. // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. // Of course, this is not absolute, just this way is relatively simple. :) external: externalPackages, }, }, }, }, { // Shortcut of `build.rollupOptions.input`. // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. entry: "electron/preload/index.ts", onstart({reload}) { // Notify the Renderer process to reload the page when the Preload scripts build is complete, // instead of restarting the entire Electron App. reload(); }, vite: { build: { target: "es2015", sourcemap: undefined, // #332 minify: minify, outDir: "dist-electron/preload", lib: { formats: ["cjs"], fileName: "index", }, rollupOptions: { external: externalPackages, output: { format: "cjs", // entryFileNames: '[name].cjs', strict: true, }, }, }, }, }, { // Shortcut of `build.rollupOptions.input`. // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. entry: "electron/preload/plugin.ts", onstart({reload}) { // Notify the Renderer process to reload the page when the Preload scripts build is complete, // instead of restarting the entire Electron App. reload(); }, vite: { build: { target: "es2015", sourcemap: undefined, // #332 minify: minify, outDir: "dist-electron/preload-plugin", lib: { formats: ["cjs"], fileName: "plugin", }, rollupOptions: { external: externalPackages, output: { format: "cjs", // entryFileNames: '[name].cjs', strict: true, }, }, }, }, }, ]), renderer(), ], build: { sourcemap: sourcemap, rollupOptions: { input: { main: path.resolve(__dirname, "index.html"), detachWindow: path.resolve(__dirname, "page/detachWindow.html"), fastPanel: path.resolve(__dirname, "page/fastPanel.html"), // 内置插件 system: path.resolve(__dirname, "page/system.html"), store: path.resolve(__dirname, "page/store.html"), workflow: path.resolve(__dirname, "page/workflow.html"), // 其他页面 about: path.resolve(__dirname, "page/about.html"), feedback: path.resolve(__dirname, "page/feedback.html"), user: path.resolve(__dirname, "page/user.html"), guide: path.resolve(__dirname, "page/guide.html"), setup: path.resolve(__dirname, "page/setup.html"), payment: path.resolve(__dirname, "page/payment.html"), monitor: path.resolve(__dirname, "page/monitor.html"), log: path.resolve(__dirname, "page/log.html"), }, }, }, server: { port: 20000, }, }; });