diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4696fd16e3..4216055bc0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,3 +11,4 @@ /packages/ui/ @MyPrototypeWhat +/app-upgrade-config.json @kangfenmao diff --git a/.github/workflows/update-app-upgrade-config.yml b/.github/workflows/update-app-upgrade-config.yml new file mode 100644 index 0000000000..7470bb0b6c --- /dev/null +++ b/.github/workflows/update-app-upgrade-config.yml @@ -0,0 +1,212 @@ +name: Update App Upgrade Config + +on: + release: + types: + - released + - prereleased + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g., v1.2.3)" + required: true + type: string + is_prerelease: + description: "Mark the tag as a prerelease when running manually" + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + propose-update: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.draft == false) + + steps: + - name: Check if should proceed + id: check + run: | + EVENT="${{ github.event_name }}" + + if [ "$EVENT" = "workflow_dispatch" ]; then + TAG="${{ github.event.inputs.tag }}" + else + TAG="${{ github.event.release.tag_name }}" + fi + + latest_tag=$( + curl -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/${{ github.repository }}/releases/latest \ + | jq -r '.tag_name' + ) + + if [ "$EVENT" = "workflow_dispatch" ]; then + MANUAL_IS_PRERELEASE="${{ github.event.inputs.is_prerelease }}" + if [ -z "$MANUAL_IS_PRERELEASE" ]; then + MANUAL_IS_PRERELEASE="false" + fi + if [ "$MANUAL_IS_PRERELEASE" = "true" ]; then + if ! echo "$TAG" | grep -E '(-beta([.-][0-9]+)?|-rc([.-][0-9]+)?)' >/dev/null; then + echo "Manual prerelease flag set but tag $TAG lacks beta/rc suffix. Skipping." >&2 + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=$MANUAL_IS_PRERELEASE" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + exit 0 + fi + + IS_PRERELEASE="${{ github.event.release.prerelease }}" + + if [ "$IS_PRERELEASE" = "true" ]; then + if ! echo "$TAG" | grep -E '(-beta([.-][0-9]+)?|-rc([.-][0-9]+)?)' >/dev/null; then + echo "Release marked as prerelease but tag $TAG lacks beta/rc suffix. Skipping." >&2 + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "Release is prerelease, proceeding" + exit 0 + fi + + if [[ "${latest_tag}" == "$TAG" ]]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "Release is latest, proceeding" + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "Release is neither prerelease nor latest, skipping" + fi + + - name: Prepare metadata + id: meta + if: steps.check.outputs.should_run == 'true' + run: | + EVENT="${{ github.event_name }}" + LATEST_TAG="${{ steps.check.outputs.latest_tag }}" + if [ "$EVENT" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + PRE="${{ github.event.release.prerelease }}" + + if [ -n "$LATEST_TAG" ] && [ "$LATEST_TAG" = "$TAG" ]; then + LATEST="true" + else + LATEST="false" + fi + TRIGGER="release" + else + TAG="${{ github.event.inputs.tag }}" + PRE="${{ github.event.inputs.is_prerelease }}" + if [ -z "$PRE" ]; then + PRE="false" + fi + if [ -n "$LATEST_TAG" ] && [ "$LATEST_TAG" = "$TAG" ] && [ "$PRE" != "true" ]; then + LATEST="true" + else + LATEST="false" + fi + TRIGGER="manual" + fi + + SAFE_TAG=$(echo "$TAG" | sed 's/[^A-Za-z0-9._-]/-/g') + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "safe_tag=$SAFE_TAG" >> "$GITHUB_OUTPUT" + echo "prerelease=$PRE" >> "$GITHUB_OUTPUT" + echo "latest=$LATEST" >> "$GITHUB_OUTPUT" + echo "trigger=$TRIGGER" >> "$GITHUB_OUTPUT" + + - name: Checkout default branch + if: steps.check.outputs.should_run == 'true' + uses: actions/checkout@v5 + with: + ref: ${{ github.event.repository.default_branch }} + path: main + fetch-depth: 0 + + - name: Checkout x-files/app-upgrade-config branch + if: steps.check.outputs.should_run == 'true' + uses: actions/checkout@v5 + with: + ref: x-files/app-upgrade-config + path: cs + fetch-depth: 0 + + - name: Setup Node.js + if: steps.check.outputs.should_run == 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Enable Corepack + if: steps.check.outputs.should_run == 'true' + run: corepack enable && corepack prepare yarn@4.9.1 --activate + + - name: Install dependencies + if: steps.check.outputs.should_run == 'true' + working-directory: main + run: yarn install --immutable + + - name: Update upgrade config + if: steps.check.outputs.should_run == 'true' + working-directory: main + env: + RELEASE_TAG: ${{ steps.meta.outputs.tag }} + IS_PRERELEASE: ${{ steps.check.outputs.is_prerelease }} + run: | + yarn tsx scripts/update-app-upgrade-config.ts \ + --tag "$RELEASE_TAG" \ + --config ../cs/app-upgrade-config.json \ + --is-prerelease "$IS_PRERELEASE" + + - name: Detect changes + if: steps.check.outputs.should_run == 'true' + id: diff + working-directory: cs + run: | + if git diff --quiet -- app-upgrade-config.json; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create pull request + if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + path: cs + base: x-files/app-upgrade-config + branch: chore/update-app-upgrade-config/${{ steps.meta.outputs.safe_tag }} + commit-message: "🤖 chore: sync app-upgrade-config for ${{ steps.meta.outputs.tag }}" + title: "chore: update app-upgrade-config for ${{ steps.meta.outputs.tag }}" + body: | + Automated update triggered by `${{ steps.meta.outputs.trigger }}`. + + - Source tag: `${{ steps.meta.outputs.tag }}` + - Pre-release: `${{ steps.meta.outputs.prerelease }}` + - Latest: `${{ steps.meta.outputs.latest }}` + - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + labels: | + automation + app-upgrade + + - name: No changes detected + if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed != 'true' + run: echo "No updates required for x-files/app-upgrade-config/app-upgrade-config.json" diff --git a/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch b/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch deleted file mode 100644 index e9ca84e6cd..0000000000 --- a/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch +++ /dev/null @@ -1,276 +0,0 @@ -diff --git a/out/macPackager.js b/out/macPackager.js -index 852f6c4d16f86a7bb8a78bf1ed5a14647a279aa1..60e7f5f16a844541eb1909b215fcda1811e924b8 100644 ---- a/out/macPackager.js -+++ b/out/macPackager.js -@@ -423,7 +423,7 @@ class MacPackager extends platformPackager_1.PlatformPackager { - } - appPlist.CFBundleName = appInfo.productName; - appPlist.CFBundleDisplayName = appInfo.productName; -- const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion; -+ const minimumSystemVersion = this.platformSpecificBuildOptions.LSMinimumSystemVersion; - if (minimumSystemVersion != null) { - appPlist.LSMinimumSystemVersion = minimumSystemVersion; - } -diff --git a/out/publish/updateInfoBuilder.js b/out/publish/updateInfoBuilder.js -index 7924c5b47d01f8dfccccb8f46658015fa66da1f7..1a1588923c3939ae1297b87931ba83f0ebc052d8 100644 ---- a/out/publish/updateInfoBuilder.js -+++ b/out/publish/updateInfoBuilder.js -@@ -133,6 +133,7 @@ async function createUpdateInfo(version, event, releaseInfo) { - const customUpdateInfo = event.updateInfo; - const url = path.basename(event.file); - const sha512 = (customUpdateInfo == null ? null : customUpdateInfo.sha512) || (await (0, hash_1.hashFile)(event.file)); -+ const minimumSystemVersion = customUpdateInfo == null ? null : customUpdateInfo.minimumSystemVersion; - const files = [{ url, sha512 }]; - const result = { - // @ts-ignore -@@ -143,9 +144,13 @@ async function createUpdateInfo(version, event, releaseInfo) { - path: url /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */, - // @ts-ignore - sha512 /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */, -+ minimumSystemVersion, - ...releaseInfo, - }; - if (customUpdateInfo != null) { -+ if (customUpdateInfo.minimumSystemVersion) { -+ delete customUpdateInfo.minimumSystemVersion; -+ } - // file info or nsis web installer packages info - Object.assign("sha512" in customUpdateInfo ? files[0] : result, customUpdateInfo); - } -diff --git a/out/targets/ArchiveTarget.js b/out/targets/ArchiveTarget.js -index e1f52a5fa86fff6643b2e57eaf2af318d541f865..47cc347f154a24b365e70ae5e1f6d309f3582ed0 100644 ---- a/out/targets/ArchiveTarget.js -+++ b/out/targets/ArchiveTarget.js -@@ -69,6 +69,9 @@ class ArchiveTarget extends core_1.Target { - } - } - } -+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) { -+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion; -+ } - await packager.info.emitArtifactBuildCompleted({ - updateInfo, - file: artifactPath, -diff --git a/out/targets/nsis/NsisTarget.js b/out/targets/nsis/NsisTarget.js -index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01de4b89b1 100644 ---- a/out/targets/nsis/NsisTarget.js -+++ b/out/targets/nsis/NsisTarget.js -@@ -305,6 +305,9 @@ class NsisTarget extends core_1.Target { - if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) { - updateInfo.isAdminRightsRequired = true; - } -+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) { -+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion; -+ } - await packager.info.emitArtifactBuildCompleted({ - file: installerPath, - updateInfo, -diff --git a/out/util/yarn.js b/out/util/yarn.js -index 1ee20f8b252a8f28d0c7b103789cf0a9a427aec1..c2878ec54d57da50bf14225e0c70c9c88664eb8a 100644 ---- a/out/util/yarn.js -+++ b/out/util/yarn.js -@@ -140,6 +140,7 @@ async function rebuild(config, { appDir, projectDir }, options) { - arch, - platform, - buildFromSource, -+ ignoreModules: config.excludeReBuildModules || undefined, - projectRootPath: projectDir, - mode: config.nativeRebuilder || "sequential", - disablePreGypCopy: true, -diff --git a/scheme.json b/scheme.json -index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..0167441bf928a92f59b5dbe70b2317a74dda74c9 100644 ---- a/scheme.json -+++ b/scheme.json -@@ -1825,6 +1825,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableArgs": { - "anyOf": [ - { -@@ -1975,6 +1989,13 @@ - ], - "description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing." - }, -+ "minimumSystemVersion": { -+ "description": "The minimum os kernel version required to install the application.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "packageCategory": { - "description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place", - "type": [ -@@ -2327,6 +2348,13 @@ - "MacConfiguration": { - "additionalProperties": false, - "properties": { -+ "LSMinimumSystemVersion": { -+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "additionalArguments": { - "anyOf": [ - { -@@ -2527,6 +2555,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -2737,7 +2779,7 @@ - "type": "boolean" - }, - "minimumSystemVersion": { -- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "description": "The minimum os kernel version required to install the application.", - "type": [ - "null", - "string" -@@ -2959,6 +3001,13 @@ - "MasConfiguration": { - "additionalProperties": false, - "properties": { -+ "LSMinimumSystemVersion": { -+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "additionalArguments": { - "anyOf": [ - { -@@ -3159,6 +3208,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -3369,7 +3432,7 @@ - "type": "boolean" - }, - "minimumSystemVersion": { -- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "description": "The minimum os kernel version required to install the application.", - "type": [ - "null", - "string" -@@ -6381,6 +6444,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -6507,6 +6584,13 @@ - "string" - ] - }, -+ "minimumSystemVersion": { -+ "description": "The minimum os kernel version required to install the application.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "protocols": { - "anyOf": [ - { -@@ -7153,6 +7237,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -7376,6 +7474,13 @@ - ], - "description": "MAS (Mac Application Store) development options (`mas-dev` target)." - }, -+ "minimumSystemVersion": { -+ "description": "The minimum os kernel version required to install the application.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "msi": { - "anyOf": [ - { diff --git a/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch b/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch new file mode 100644 index 0000000000..f9e54ac947 --- /dev/null +++ b/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch @@ -0,0 +1,14 @@ +diff --git a/out/util.js b/out/util.js +index 9294ffd6ca8f02c2e0f90c663e7e9cdc02c1ac37..f52107493e2995320ee4efd0eb2a8c9bf03291a2 100644 +--- a/out/util.js ++++ b/out/util.js +@@ -23,7 +23,8 @@ function newUrlFromBase(pathname, baseUrl, addRandomQueryToAvoidCaching = false) + result.search = search; + } + else if (addRandomQueryToAvoidCaching) { +- result.search = `noCache=${Date.now().toString(32)}`; ++ // use no cache header instead ++ // result.search = `noCache=${Date.now().toString(32)}`; + } + return result; + } diff --git a/app-upgrade-config.json b/app-upgrade-config.json new file mode 100644 index 0000000000..84e381c86a --- /dev/null +++ b/app-upgrade-config.json @@ -0,0 +1,49 @@ +{ + "lastUpdated": "2025-11-10T08:14:28Z", + "versions": { + "1.6.7": { + "metadata": { + "segmentId": "legacy-v1", + "segmentType": "legacy" + }, + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channels": { + "latest": { + "version": "1.6.7", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7", + "gitcode": "https://releases.cherry-ai.com" + } + }, + "rc": { + "version": "1.6.0-rc.5", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5" + } + }, + "beta": { + "version": "1.7.0-beta.3", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3" + } + } + } + }, + "2.0.0": { + "metadata": { + "segmentId": "gateway-v2", + "segmentType": "breaking" + }, + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channels": { + "latest": null, + "rc": null, + "beta": null + } + } + } +} diff --git a/config/app-upgrade-segments.json b/config/app-upgrade-segments.json new file mode 100644 index 0000000000..70c8ac25f0 --- /dev/null +++ b/config/app-upgrade-segments.json @@ -0,0 +1,81 @@ +{ + "segments": [ + { + "id": "legacy-v1", + "type": "legacy", + "match": { + "range": ">=1.0.0 <2.0.0" + }, + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://releases.cherry-ai.com" + } + }, + "rc": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + }, + "beta": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + }, + { + "id": "gateway-v2", + "type": "breaking", + "match": { + "exact": ["2.0.0"] + }, + "lockedVersion": "2.0.0", + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + }, + { + "id": "current-v2", + "type": "latest", + "match": { + "range": ">=2.0.0 <3.0.0", + "excludeExact": ["2.0.0"] + }, + "minCompatibleVersion": "2.0.0", + "description": "Current latest v2.x release", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + }, + "rc": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + }, + "beta": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + } + ] +} diff --git a/docs/technical/app-upgrade-config-en.md b/docs/technical/app-upgrade-config-en.md new file mode 100644 index 0000000000..0662abf236 --- /dev/null +++ b/docs/technical/app-upgrade-config-en.md @@ -0,0 +1,430 @@ +# Update Configuration System Design Document + +## Background + +Currently, AppUpdater directly queries the GitHub API to retrieve beta and rc update information. To support users in China, we need to fetch a static JSON configuration file from GitHub/GitCode based on IP geolocation, which contains update URLs for all channels. + +## Design Goals + +1. Support different configuration sources based on IP geolocation (GitHub/GitCode) +2. Support version compatibility control (e.g., users below v1.x must upgrade to v1.7.0 before upgrading to v2.0) +3. Easy to extend, supporting future multi-major-version upgrade paths (v1.6 → v1.7 → v2.0 → v2.8 → v3.0) +4. Maintain compatibility with existing electron-updater mechanism + +## Current Version Strategy + +- **v1.7.x** is the last version of the 1.x series +- Users **below v1.7.0** must first upgrade to v1.7.0 (or higher 1.7.x version) +- Users **v1.7.0 and above** can directly upgrade to v2.x.x + +## Automation Workflow + +The `x-files/app-upgrade-config/app-upgrade-config.json` file is synchronized by the [`Update App Upgrade Config`](../../.github/workflows/update-app-upgrade-config.yml) workflow. The workflow runs the [`scripts/update-app-upgrade-config.ts`](../../scripts/update-app-upgrade-config.ts) helper so that every release tag automatically updates the JSON in `x-files/app-upgrade-config`. + +### Trigger Conditions + +- **Release events (`release: released/prereleased`)** + - Draft releases are ignored. + - When GitHub marks the release as _prerelease_, the tag must include `-beta`/`-rc` (with optional numeric suffix). Otherwise the workflow exits early. + - When GitHub marks the release as stable, the tag must match the latest release returned by the GitHub API. This prevents out-of-order updates when publishing historical tags. + - If the guard clauses pass, the version is tagged as `latest` or `beta/rc` based on its semantic suffix and propagated to the script through the `IS_PRERELEASE` flag. +- **Manual dispatch (`workflow_dispatch`)** + - Required input: `tag` (e.g., `v2.0.1`). Optional input: `is_prerelease` (defaults to `false`). + - When `is_prerelease=true`, the tag must carry a beta/rc suffix, mirroring the automatic validation. + - Manual runs still download the latest release metadata so that the workflow knows whether the tag represents the newest stable version (for documentation inside the PR body). + +### Workflow Steps + +1. **Guard + metadata preparation** – the `Check if should proceed` and `Prepare metadata` steps compute the target tag, prerelease flag, whether the tag is the newest release, and a `safe_tag` slug used for branch names. When any rule fails, the workflow stops without touching the config. +2. **Checkout source branches** – the default branch is checked out into `main/`, while the long-lived `x-files/app-upgrade-config` branch lives in `cs/`. All modifications happen in the latter directory. +3. **Install toolchain** – Node.js 22, Corepack, and frozen Yarn dependencies are installed inside `main/`. +4. **Run the update script** – `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json --is-prerelease ` updates the JSON in-place. + - The script normalizes the tag (e.g., strips `v` prefix), detects the release channel (`latest`, `rc`, `beta`), and loads segment rules from `config/app-upgrade-segments.json`. + - It validates that prerelease flags and semantic suffixes agree, enforces locked segments, builds mirror feed URLs, and performs release-availability checks (GitHub HEAD request for every channel; GitCode GET for latest channels, falling back to `https://releases.cherry-ai.com` when gitcode is delayed). + - After updating the relevant channel entry, the script rewrites the config with semver-sort order and a new `lastUpdated` timestamp. +5. **Detect changes + create PR** – if `cs/app-upgrade-config.json` changed, the workflow opens a PR `chore/update-app-upgrade-config/` against `x-files/app-upgrade-config` with a commit message `🤖 chore: sync app-upgrade-config for `. Otherwise it logs that no update is required. + +### Manual Trigger Guide + +1. Open the Cherry Studio repository on GitHub → **Actions** tab → select **Update App Upgrade Config**. +2. Click **Run workflow**, choose the default branch (usually `main`), and fill in the `tag` input (e.g., `v2.1.0`). +3. Toggle `is_prerelease` only when the tag carries a prerelease suffix (`-beta`, `-rc`). Leave it unchecked for stable releases. +4. Start the run and wait for it to finish. Check the generated PR in the `x-files/app-upgrade-config` branch, verify the diff in `app-upgrade-config.json`, and merge once validated. + +## JSON Configuration File Format + +### File Location + +- **GitHub**: `https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/x-files/app-upgrade-config/app-upgrade-config.json` +- **GitCode**: `https://gitcode.com/CherryHQ/cherry-studio/raw/x-files/app-upgrade-config/app-upgrade-config.json` + +**Note**: Both mirrors provide the same configuration file hosted on the `x-files/app-upgrade-config` branch. The client automatically selects the optimal mirror based on IP geolocation. + +### Configuration Structure (Current Implementation) + +```json +{ + "lastUpdated": "2025-01-05T00:00:00Z", + "versions": { + "1.6.7": { + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channels": { + "latest": { + "version": "1.6.7", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v1.6.7" + } + }, + "rc": { + "version": "1.6.0-rc.5", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5" + } + }, + "beta": { + "version": "1.6.7-beta.3", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3" + } + } + } + }, + "2.0.0": { + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channels": { + "latest": null, + "rc": null, + "beta": null + } + } + } +} +``` + +### Future Extension Example + +When releasing v3.0, if users need to first upgrade to v2.8, you can add: + +```json +{ + "2.8.0": { + "minCompatibleVersion": "2.0.0", + "description": "Stable v2.8 - required for v3 upgrade", + "channels": { + "latest": { + "version": "2.8.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v2.8.0", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v2.8.0" + } + }, + "rc": null, + "beta": null + } + }, + "3.0.0": { + "minCompatibleVersion": "2.8.0", + "description": "Major release v3.0", + "channels": { + "latest": { + "version": "3.0.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/latest", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/latest" + } + }, + "rc": { + "version": "3.0.0-rc.1", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1" + } + }, + "beta": null + } + } +} +``` + +### Field Descriptions + +- `lastUpdated`: Last update time of the configuration file (ISO 8601 format) +- `versions`: Version configuration object, key is the version number, sorted by semantic versioning + - `minCompatibleVersion`: Minimum compatible version that can upgrade to this version + - `description`: Version description + - `channels`: Update channel configuration + - `latest`: Stable release channel + - `rc`: Release Candidate channel + - `beta`: Beta testing channel + - Each channel contains: + - `version`: Version number for this channel + - `feedUrls`: Multi-mirror URL configuration + - `github`: electron-updater feed URL for GitHub mirror + - `gitcode`: electron-updater feed URL for GitCode mirror + - `metadata`: Stable mapping info for automation + - `segmentId`: ID from `config/app-upgrade-segments.json` + - `segmentType`: Optional flag (`legacy` | `breaking` | `latest`) for documentation/debugging + +## TypeScript Type Definitions + +```typescript +// Mirror enum +enum UpdateMirror { + GITHUB = 'github', + GITCODE = 'gitcode' +} + +interface UpdateConfig { + lastUpdated: string + versions: { + [versionKey: string]: VersionConfig + } +} + +interface VersionConfig { + minCompatibleVersion: string + description: string + channels: { + latest: ChannelConfig | null + rc: ChannelConfig | null + beta: ChannelConfig | null + } + metadata?: { + segmentId: string + segmentType?: 'legacy' | 'breaking' | 'latest' + } +} + +interface ChannelConfig { + version: string + feedUrls: Record + // Equivalent to: + // feedUrls: { + // github: string + // gitcode: string + // } +} +``` + +## Segment Metadata & Breaking Markers + +- **Segment definitions** now live in `config/app-upgrade-segments.json`. Each segment describes a semantic-version range (or exact matches) plus metadata such as `segmentId`, `segmentType`, `minCompatibleVersion`, and per-channel feed URL templates. +- Each entry under `versions` carries a `metadata.segmentId`. This acts as the stable key that scripts use to decide which slot to update, even if the actual semantic version string changes. +- Mark major upgrade gateways (e.g., `2.0.0`) by giving the related segment a `segmentType: "breaking"` and (optionally) `lockedVersion`. This prevents automation from accidentally moving that entry when other 2.x builds ship. +- Adding another breaking hop (e.g., `3.0.0`) only requires defining a new segment in the JSON file; the automation will pick it up on the next run. + +## Automation Workflow + +Starting from this change, `.github/workflows/update-app-upgrade-config.yml` listens to GitHub release events (published + prerelease). The workflow: + +1. Checks out the default branch (for scripts) and the `x-files/app-upgrade-config` branch (where the config is hosted). +2. Runs `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json` to regenerate the config directly inside the `x-files/app-upgrade-config` working tree. +3. If the file changed, it opens a PR against `x-files/app-upgrade-config` via `peter-evans/create-pull-request`, with the generated diff limited to `app-upgrade-config.json`. + +You can run the same script locally via `yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json` (add `--dry-run` to preview) to reproduce or debug whatever the workflow does. Passing `--skip-release-checks` along with `--dry-run` lets you bypass the release-page existence check (useful when the GitHub/GitCode pages aren’t published yet). Running without `--config` continues to update the copy in your current working directory (main branch) for documentation purposes. + +## Version Matching Logic + +### Algorithm Flow + +1. Get user's current version (`currentVersion`) and requested channel (`requestedChannel`) +2. Get all version numbers from configuration file, sort in descending order by semantic versioning +3. Iterate through the sorted version list: + - Check if `currentVersion >= minCompatibleVersion` + - Check if the requested `channel` exists and is not `null` + - If conditions are met, return the channel configuration +4. If no matching version is found, return `null` + +### Pseudocode Implementation + +```typescript +function findCompatibleVersion( + currentVersion: string, + requestedChannel: UpgradeChannel, + config: UpdateConfig +): ChannelConfig | null { + // Get all version numbers and sort in descending order + const versions = Object.keys(config.versions).sort(semver.rcompare) + + for (const versionKey of versions) { + const versionConfig = config.versions[versionKey] + const channelConfig = versionConfig.channels[requestedChannel] + + // Check version compatibility and channel availability + if ( + semver.gte(currentVersion, versionConfig.minCompatibleVersion) && + channelConfig !== null + ) { + return channelConfig + } + } + + return null // No compatible version found +} +``` + +## Upgrade Path Examples + +### Scenario 1: v1.6.5 User Upgrade (Below 1.7) + +- **Current Version**: 1.6.5 +- **Requested Channel**: latest +- **Match Result**: 1.7.0 +- **Reason**: 1.6.5 >= 0.0.0 (satisfies 1.7.0's minCompatibleVersion), but doesn't satisfy 2.0.0's minCompatibleVersion (1.7.0) +- **Action**: Prompt user to upgrade to 1.7.0, which is the required intermediate version for v2.x upgrade + +### Scenario 2: v1.6.5 User Requests rc/beta + +- **Current Version**: 1.6.5 +- **Requested Channel**: rc or beta +- **Match Result**: 1.7.0 (latest) +- **Reason**: 1.7.0 version doesn't provide rc/beta channels (values are null) +- **Action**: Upgrade to 1.7.0 stable version + +### Scenario 3: v1.7.0 User Upgrades to Latest + +- **Current Version**: 1.7.0 +- **Requested Channel**: latest +- **Match Result**: 2.0.0 +- **Reason**: 1.7.0 >= 1.7.0 (satisfies 2.0.0's minCompatibleVersion) +- **Action**: Directly upgrade to 2.0.0 (current latest stable version) + +### Scenario 4: v1.7.2 User Upgrades to RC Version + +- **Current Version**: 1.7.2 +- **Requested Channel**: rc +- **Match Result**: 2.0.0-rc.1 +- **Reason**: 1.7.2 >= 1.7.0 (satisfies 2.0.0's minCompatibleVersion), and rc channel exists +- **Action**: Upgrade to 2.0.0-rc.1 + +### Scenario 5: v1.7.0 User Upgrades to Beta Version + +- **Current Version**: 1.7.0 +- **Requested Channel**: beta +- **Match Result**: 2.0.0-beta.1 +- **Reason**: 1.7.0 >= 1.7.0, and beta channel exists +- **Action**: Upgrade to 2.0.0-beta.1 + +### Scenario 6: v2.5.0 User Upgrade (Future) + +Assuming v2.8.0 and v3.0.0 configurations have been added: +- **Current Version**: 2.5.0 +- **Requested Channel**: latest +- **Match Result**: 2.8.0 +- **Reason**: 2.5.0 >= 2.0.0 (satisfies 2.8.0's minCompatibleVersion), but doesn't satisfy 3.0.0's requirement +- **Action**: Prompt user to upgrade to 2.8.0, which is the required intermediate version for v3.x upgrade + +## Code Changes + +### Main Modifications + +1. **New Methods** + - `_fetchUpdateConfig(ipCountry: string): Promise` - Fetch configuration file based on IP + - `_findCompatibleChannel(currentVersion: string, channel: UpgradeChannel, config: UpdateConfig): ChannelConfig | null` - Find compatible channel configuration + +2. **Modified Methods** + - `_getReleaseVersionFromGithub()` → Remove or refactor to `_getChannelFeedUrl()` + - `_setFeedUrl()` - Use new configuration system to replace existing logic + +3. **New Type Definitions** + - `UpdateConfig` + - `VersionConfig` + - `ChannelConfig` + +### Mirror Selection Logic + +The client automatically selects the optimal mirror based on IP geolocation: + +```typescript +private async _setFeedUrl() { + const currentVersion = app.getVersion() + const testPlan = configManager.getTestPlan() + const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST + + // Determine mirror based on IP country + const ipCountry = await getIpCountry() + const mirror = ipCountry.toLowerCase() === 'cn' ? 'gitcode' : 'github' + + // Fetch update config + const config = await this._fetchUpdateConfig(mirror) + + if (config) { + const channelConfig = this._findCompatibleChannel(currentVersion, requestedChannel, config) + if (channelConfig) { + // Select feed URL from the corresponding mirror + const feedUrl = channelConfig.feedUrls[mirror] + this._setChannel(requestedChannel, feedUrl) + return + } + } + + // Fallback logic + const defaultFeedUrl = mirror === 'gitcode' + ? FeedUrl.PRODUCTION + : FeedUrl.GITHUB_LATEST + this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl) +} + +private async _fetchUpdateConfig(mirror: 'github' | 'gitcode'): Promise { + const configUrl = mirror === 'gitcode' + ? UpdateConfigUrl.GITCODE + : UpdateConfigUrl.GITHUB + + try { + const response = await net.fetch(configUrl, { + headers: { + 'User-Agent': generateUserAgent(), + 'Accept': 'application/json', + 'X-Client-Id': configManager.getClientId() + } + }) + return await response.json() as UpdateConfig + } catch (error) { + logger.error('Failed to fetch update config:', error) + return null + } +} +``` + +## Fallback and Error Handling Strategy + +1. **Configuration file fetch failure**: Log error, return current version, don't offer updates +2. **No matching version**: Notify user that current version doesn't support automatic upgrade +3. **Network exception**: Cache last successfully fetched configuration (optional) + +## GitHub Release Requirements + +To support intermediate version upgrades, the following files need to be retained: + +- **v1.7.0 release** and its latest*.yml files (as upgrade target for users below v1.7) +- Future intermediate versions (e.g., v2.8.0) need to retain corresponding release and latest*.yml files +- Complete installation packages for each version + +### Currently Required Releases + +| Version | Purpose | Must Retain | +|---------|---------|-------------| +| v1.7.0 | Upgrade target for users below 1.7 | ✅ Yes | +| v2.0.0-rc.1 | RC testing channel | ❌ Optional | +| v2.0.0-beta.1 | Beta testing channel | ❌ Optional | +| latest | Latest stable version (automatic) | ✅ Yes | + +## Advantages + +1. **Flexibility**: Supports arbitrarily complex upgrade paths +2. **Extensibility**: Adding new versions only requires adding new entries to the configuration file +3. **Maintainability**: Configuration is separated from code, allowing upgrade strategy adjustments without releasing new versions +4. **Multi-source support**: Automatically selects optimal configuration source based on geolocation +5. **Version control**: Enforces intermediate version upgrades, ensuring data migration and compatibility + +## Future Extensions + +- Support more granular version range control (e.g., `>=1.5.0 <1.8.0`) +- Support multi-step upgrade path hints (e.g., notify user needs 1.5 → 1.8 → 2.0) +- Support A/B testing and gradual rollout +- Support local caching and expiration strategy for configuration files diff --git a/docs/technical/app-upgrade-config-zh.md b/docs/technical/app-upgrade-config-zh.md new file mode 100644 index 0000000000..29f9f75d79 --- /dev/null +++ b/docs/technical/app-upgrade-config-zh.md @@ -0,0 +1,430 @@ +# 更新配置系统设计文档 + +## 背景 + +当前 AppUpdater 直接请求 GitHub API 获取 beta 和 rc 的更新信息。为了支持国内用户,需要根据 IP 地理位置,分别从 GitHub/GitCode 获取一个固定的 JSON 配置文件,该文件包含所有渠道的更新地址。 + +## 设计目标 + +1. 支持根据 IP 地理位置选择不同的配置源(GitHub/GitCode) +2. 支持版本兼容性控制(如 v1.x 以下必须先升级到 v1.7.0 才能升级到 v2.0) +3. 易于扩展,支持未来多个主版本的升级路径(v1.6 → v1.7 → v2.0 → v2.8 → v3.0) +4. 保持与现有 electron-updater 机制的兼容性 + +## 当前版本策略 + +- **v1.7.x** 是 1.x 系列的最后版本 +- **v1.7.0 以下**的用户必须先升级到 v1.7.0(或更高的 1.7.x 版本) +- **v1.7.0 及以上**的用户可以直接升级到 v2.x.x + +## 自动化工作流 + +`x-files/app-upgrade-config/app-upgrade-config.json` 由 [`Update App Upgrade Config`](../../.github/workflows/update-app-upgrade-config.yml) workflow 自动同步。工作流会调用 [`scripts/update-app-upgrade-config.ts`](../../scripts/update-app-upgrade-config.ts) 脚本,根据指定 tag 更新 `x-files/app-upgrade-config` 分支上的配置文件。 + +### 触发条件 + +- **Release 事件(`release: released/prereleased`)** + - Draft release 会被忽略。 + - 当 GitHub 将 release 标记为 *prerelease* 时,tag 必须包含 `-beta`/`-rc`(可带序号),否则直接跳过。 + - 当 release 标记为稳定版时,tag 必须与 GitHub API 返回的最新稳定版本一致,防止发布历史 tag 时意外挂起工作流。 + - 满足上述条件后,工作流会根据语义化版本判断渠道(`latest`/`beta`/`rc`),并通过 `IS_PRERELEASE` 传递给脚本。 +- **手动触发(`workflow_dispatch`)** + - 必填:`tag`(例:`v2.0.1`);选填:`is_prerelease`(默认 `false`)。 + - 当 `is_prerelease=true` 时,同样要求 tag 带有 beta/rc 后缀。 + - 手动运行仍会请求 GitHub 最新 release 信息,用于在 PR 说明中标注该 tag 是否是最新稳定版。 + +### 工作流步骤 + +1. **检查与元数据准备**:`Check if should proceed` 和 `Prepare metadata` 步骤会计算 tag、prerelease 标志、是否最新版本以及用于分支名的 `safe_tag`。若任意校验失败,工作流立即退出。 +2. **检出分支**:默认分支被检出到 `main/`,长期维护的 `x-files/app-upgrade-config` 分支则在 `cs/` 中,所有改动都发生在 `cs/`。 +3. **安装工具链**:安装 Node.js 22、启用 Corepack,并在 `main/` 目录执行 `yarn install --immutable`。 +4. **运行更新脚本**:执行 `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json --is-prerelease `。 + - 脚本会标准化 tag(去掉 `v` 前缀等)、识别渠道、加载 `config/app-upgrade-segments.json` 中的分段规则。 + - 校验 prerelease 标志与语义后缀是否匹配、强制锁定的 segment 是否满足、生成镜像的下载地址,并检查 release 是否已经在 GitHub/GitCode 可用(latest 渠道在 GitCode 不可用时会回退到 `https://releases.cherry-ai.com`)。 + - 更新对应的渠道配置后,脚本会按 semver 排序写回 JSON,并刷新 `lastUpdated`。 +5. **检测变更并创建 PR**:若 `cs/app-upgrade-config.json` 有变更,则创建 `chore/update-app-upgrade-config/` 分支,提交信息为 `🤖 chore: sync app-upgrade-config for `,并向 `x-files/app-upgrade-config` 提 PR;无变更则输出提示。 + +### 手动触发指南 + +1. 进入 Cherry Studio 仓库的 GitHub **Actions** 页面,选择 **Update App Upgrade Config** 工作流。 +2. 点击 **Run workflow**,保持默认分支(通常为 `main`),填写 `tag`(如 `v2.1.0`)。 +3. 只有在 tag 带 `-beta`/`-rc` 后缀时才勾选 `is_prerelease`,稳定版保持默认。 +4. 启动运行并等待完成,随后到 `x-files/app-upgrade-config` 分支的 PR 查看 `app-upgrade-config.json` 的变更并在验证后合并。 + +## JSON 配置文件格式 + +### 文件位置 + +- **GitHub**: `https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/x-files/app-upgrade-config/app-upgrade-config.json` +- **GitCode**: `https://gitcode.com/CherryHQ/cherry-studio/raw/x-files/app-upgrade-config/app-upgrade-config.json` + +**说明**:两个镜像源提供相同的配置文件,统一托管在 `x-files/app-upgrade-config` 分支上。客户端根据 IP 地理位置自动选择最优镜像源。 + +### 配置结构(当前实际配置) + +```json +{ + "lastUpdated": "2025-01-05T00:00:00Z", + "versions": { + "1.6.7": { + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channels": { + "latest": { + "version": "1.6.7", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v1.6.7" + } + }, + "rc": { + "version": "1.6.0-rc.5", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5" + } + }, + "beta": { + "version": "1.6.7-beta.3", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3" + } + } + } + }, + "2.0.0": { + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channels": { + "latest": null, + "rc": null, + "beta": null + } + } + } +} +``` + +### 未来扩展示例 + +当需要发布 v3.0 时,如果需要强制用户先升级到 v2.8,可以添加: + +```json +{ + "2.8.0": { + "minCompatibleVersion": "2.0.0", + "description": "Stable v2.8 - required for v3 upgrade", + "channels": { + "latest": { + "version": "2.8.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v2.8.0", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v2.8.0" + } + }, + "rc": null, + "beta": null + } + }, + "3.0.0": { + "minCompatibleVersion": "2.8.0", + "description": "Major release v3.0", + "channels": { + "latest": { + "version": "3.0.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/latest", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/latest" + } + }, + "rc": { + "version": "3.0.0-rc.1", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1" + } + }, + "beta": null + } + } +} +``` + +### 字段说明 + +- `lastUpdated`: 配置文件最后更新时间(ISO 8601 格式) +- `versions`: 版本配置对象,key 为版本号,按语义化版本排序 + - `minCompatibleVersion`: 可以升级到此版本的最低兼容版本 + - `description`: 版本描述 + - `channels`: 更新渠道配置 + - `latest`: 稳定版渠道 + - `rc`: Release Candidate 渠道 + - `beta`: Beta 测试渠道 + - 每个渠道包含: + - `version`: 该渠道的版本号 + - `feedUrls`: 多镜像源 URL 配置 + - `github`: GitHub 镜像源的 electron-updater feed URL + - `gitcode`: GitCode 镜像源的 electron-updater feed URL + - `metadata`: 自动化匹配所需的稳定标识 + - `segmentId`: 来自 `config/app-upgrade-segments.json` 的段位 ID + - `segmentType`: 可选字段(`legacy` | `breaking` | `latest`),便于文档/调试 + +## TypeScript 类型定义 + +```typescript +// 镜像源枚举 +enum UpdateMirror { + GITHUB = 'github', + GITCODE = 'gitcode' +} + +interface UpdateConfig { + lastUpdated: string + versions: { + [versionKey: string]: VersionConfig + } +} + +interface VersionConfig { + minCompatibleVersion: string + description: string + channels: { + latest: ChannelConfig | null + rc: ChannelConfig | null + beta: ChannelConfig | null + } + metadata?: { + segmentId: string + segmentType?: 'legacy' | 'breaking' | 'latest' + } +} + +interface ChannelConfig { + version: string + feedUrls: Record + // 等同于: + // feedUrls: { + // github: string + // gitcode: string + // } +} +``` + +## 段位元数据(Break Change 标记) + +- 所有段位定义(如 `legacy-v1`、`gateway-v2` 等)集中在 `config/app-upgrade-segments.json`,用于描述匹配范围、`segmentId`、`segmentType`、默认 `minCompatibleVersion/description` 以及各渠道的 URL 模板。 +- `versions` 下的每个节点都会带上 `metadata.segmentId`。自动脚本始终依据该 ID 来定位并更新条目,即便 key 从 `2.1.5` 切换到 `2.1.6` 也不会错位。 +- 如果某段需要锁死在特定版本(例如 `2.0.0` 的 break change),可在段定义中设置 `segmentType: "breaking"` 并提供 `lockedVersion`,脚本在遇到不匹配的 tag 时会短路报错,保证升级路径安全。 +- 面对未来新的断层(例如 `3.0.0`),只需要在段定义里新增一段,自动化即可识别并更新。 + +## 自动化工作流 + +`.github/workflows/update-app-upgrade-config.yml` 会在 GitHub Release(包含正常发布与 Pre Release)触发: + +1. 同时 Checkout 仓库默认分支(用于脚本)和 `x-files/app-upgrade-config` 分支(真实托管配置的分支)。 +2. 在默认分支目录执行 `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json`,直接重写 `x-files/app-upgrade-config` 分支里的配置文件。 +3. 如果 `app-upgrade-config.json` 有变化,则通过 `peter-evans/create-pull-request` 自动创建一个指向 `x-files/app-upgrade-config` 的 PR,Diff 仅包含该文件。 + +如需本地调试,可执行 `yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json`(加 `--dry-run` 仅打印结果)来复现 CI 行为。若需要暂时跳过 GitHub/GitCode Release 页面是否就绪的校验,可在 `--dry-run` 的同时附加 `--skip-release-checks`。不加 `--config` 时默认更新当前工作目录(通常是 main 分支)下的副本,方便文档/审查。 + +## 版本匹配逻辑 + +### 算法流程 + +1. 获取用户当前版本(`currentVersion`)和请求的渠道(`requestedChannel`) +2. 获取配置文件中所有版本号,按语义化版本从大到小排序 +3. 遍历排序后的版本列表: + - 检查 `currentVersion >= minCompatibleVersion` + - 检查请求的 `channel` 是否存在且不为 `null` + - 如果满足条件,返回该渠道配置 +4. 如果没有找到匹配版本,返回 `null` + +### 伪代码实现 + +```typescript +function findCompatibleVersion( + currentVersion: string, + requestedChannel: UpgradeChannel, + config: UpdateConfig +): ChannelConfig | null { + // 获取所有版本号并从大到小排序 + const versions = Object.keys(config.versions).sort(semver.rcompare) + + for (const versionKey of versions) { + const versionConfig = config.versions[versionKey] + const channelConfig = versionConfig.channels[requestedChannel] + + // 检查版本兼容性和渠道可用性 + if ( + semver.gte(currentVersion, versionConfig.minCompatibleVersion) && + channelConfig !== null + ) { + return channelConfig + } + } + + return null // 没有找到兼容版本 +} +``` + +## 升级路径示例 + +### 场景 1: v1.6.5 用户升级(低于 1.7) + +- **当前版本**: 1.6.5 +- **请求渠道**: latest +- **匹配结果**: 1.7.0 +- **原因**: 1.6.5 >= 0.0.0(满足 1.7.0 的 minCompatibleVersion),但不满足 2.0.0 的 minCompatibleVersion (1.7.0) +- **操作**: 提示用户升级到 1.7.0,这是升级到 v2.x 的必要中间版本 + +### 场景 2: v1.6.5 用户请求 rc/beta + +- **当前版本**: 1.6.5 +- **请求渠道**: rc 或 beta +- **匹配结果**: 1.7.0 (latest) +- **原因**: 1.7.0 版本不提供 rc/beta 渠道(值为 null) +- **操作**: 升级到 1.7.0 稳定版 + +### 场景 3: v1.7.0 用户升级到最新版 + +- **当前版本**: 1.7.0 +- **请求渠道**: latest +- **匹配结果**: 2.0.0 +- **原因**: 1.7.0 >= 1.7.0(满足 2.0.0 的 minCompatibleVersion) +- **操作**: 直接升级到 2.0.0(当前最新稳定版) + +### 场景 4: v1.7.2 用户升级到 RC 版本 + +- **当前版本**: 1.7.2 +- **请求渠道**: rc +- **匹配结果**: 2.0.0-rc.1 +- **原因**: 1.7.2 >= 1.7.0(满足 2.0.0 的 minCompatibleVersion),且 rc 渠道存在 +- **操作**: 升级到 2.0.0-rc.1 + +### 场景 5: v1.7.0 用户升级到 Beta 版本 + +- **当前版本**: 1.7.0 +- **请求渠道**: beta +- **匹配结果**: 2.0.0-beta.1 +- **原因**: 1.7.0 >= 1.7.0,且 beta 渠道存在 +- **操作**: 升级到 2.0.0-beta.1 + +### 场景 6: v2.5.0 用户升级(未来) + +假设已添加 v2.8.0 和 v3.0.0 配置: +- **当前版本**: 2.5.0 +- **请求渠道**: latest +- **匹配结果**: 2.8.0 +- **原因**: 2.5.0 >= 2.0.0(满足 2.8.0 的 minCompatibleVersion),但不满足 3.0.0 的要求 +- **操作**: 提示用户升级到 2.8.0,这是升级到 v3.x 的必要中间版本 + +## 代码改动计划 + +### 主要修改 + +1. **新增方法** + - `_fetchUpdateConfig(ipCountry: string): Promise` - 根据 IP 获取配置文件 + - `_findCompatibleChannel(currentVersion: string, channel: UpgradeChannel, config: UpdateConfig): ChannelConfig | null` - 查找兼容的渠道配置 + +2. **修改方法** + - `_getReleaseVersionFromGithub()` → 移除或重构为 `_getChannelFeedUrl()` + - `_setFeedUrl()` - 使用新的配置系统替代现有逻辑 + +3. **新增类型定义** + - `UpdateConfig` + - `VersionConfig` + - `ChannelConfig` + +### 镜像源选择逻辑 + +客户端根据 IP 地理位置自动选择最优镜像源: + +```typescript +private async _setFeedUrl() { + const currentVersion = app.getVersion() + const testPlan = configManager.getTestPlan() + const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST + + // 根据 IP 国家确定镜像源 + const ipCountry = await getIpCountry() + const mirror = ipCountry.toLowerCase() === 'cn' ? 'gitcode' : 'github' + + // 获取更新配置 + const config = await this._fetchUpdateConfig(mirror) + + if (config) { + const channelConfig = this._findCompatibleChannel(currentVersion, requestedChannel, config) + if (channelConfig) { + // 从配置中选择对应镜像源的 URL + const feedUrl = channelConfig.feedUrls[mirror] + this._setChannel(requestedChannel, feedUrl) + return + } + } + + // Fallback 逻辑 + const defaultFeedUrl = mirror === 'gitcode' + ? FeedUrl.PRODUCTION + : FeedUrl.GITHUB_LATEST + this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl) +} + +private async _fetchUpdateConfig(mirror: 'github' | 'gitcode'): Promise { + const configUrl = mirror === 'gitcode' + ? UpdateConfigUrl.GITCODE + : UpdateConfigUrl.GITHUB + + try { + const response = await net.fetch(configUrl, { + headers: { + 'User-Agent': generateUserAgent(), + 'Accept': 'application/json', + 'X-Client-Id': configManager.getClientId() + } + }) + return await response.json() as UpdateConfig + } catch (error) { + logger.error('Failed to fetch update config:', error) + return null + } +} +``` + +## 降级和容错策略 + +1. **配置文件获取失败**: 记录错误日志,返回当前版本,不提供更新 +2. **没有匹配的版本**: 提示用户当前版本不支持自动升级 +3. **网络异常**: 缓存上次成功获取的配置(可选) + +## GitHub Release 要求 + +为支持中间版本升级,需要保留以下文件: + +- **v1.7.0 release** 及其 latest*.yml 文件(作为 v1.7 以下用户的升级目标) +- 未来如需强制中间版本(如 v2.8.0),需要保留对应的 release 和 latest*.yml 文件 +- 各版本的完整安装包 + +### 当前需要的 Release + +| 版本 | 用途 | 必须保留 | +|------|------|---------| +| v1.7.0 | 1.7 以下用户的升级目标 | ✅ 是 | +| v2.0.0-rc.1 | RC 测试渠道 | ❌ 可选 | +| v2.0.0-beta.1 | Beta 测试渠道 | ❌ 可选 | +| latest | 最新稳定版(自动) | ✅ 是 | + +## 优势 + +1. **灵活性**: 支持任意复杂的升级路径 +2. **可扩展性**: 新增版本只需在配置文件中添加新条目 +3. **可维护性**: 配置与代码分离,无需发版即可调整升级策略 +4. **多源支持**: 自动根据地理位置选择最优配置源 +5. **版本控制**: 强制中间版本升级,确保数据迁移和兼容性 + +## 未来扩展 + +- 支持更细粒度的版本范围控制(如 `>=1.5.0 <1.8.0`) +- 支持多步升级路径提示(如提示用户需要 1.5 → 1.8 → 2.0) +- 支持 A/B 测试和灰度发布 +- 支持配置文件的本地缓存和过期策略 diff --git a/electron-builder.yml b/electron-builder.yml index e63fa4f122..cdbf21408e 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -98,7 +98,6 @@ mac: entitlementsInherit: build/entitlements.mac.plist notarize: false artifactName: ${productName}-${version}-${arch}.${ext} - minimumSystemVersion: "20.1.0" # 最低支持 macOS 11.0 extendInfo: - NSCameraUsageDescription: Application requests access to the device's camera. - NSMicrophoneUsageDescription: Application requests access to the device's microphone. @@ -136,50 +135,58 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - What's New in v1.7.0-beta.5 + What's New in v1.7.0-rc.1 - New Features: - - MCPRouter Provider: Added MCPRouter provider integration with token management and server synchronization - - MCP Marketplace: Enhanced MCP server discovery and management with multi-provider marketplace support - - Agent Permission Mode Display: Visual permission mode cards in empty session states - - Assistant Subscription Settings: Added subscription URL management in assistant presets + 🎉 MAJOR NEW FEATURE: AI Agents + - Create and manage custom AI agents with specialized tools and permissions + - Dedicated agent sessions with persistent SQLite storage, separate from regular chats + - Real-time tool approval system - review and approve agent actions dynamically + - MCP (Model Context Protocol) integration for connecting external tools + - Slash commands support for quick agent interactions + - OpenAI-compatible REST API for agent access - Improvements: - - UI Optimization: Sidebar tooltip placement improved on macOS to avoid overlapping window controls - - MCP Server Logos: Display server logos in Agent settings tooling section - - Long Command Handling: Bash command tags now auto-truncate (hover to view full command for commands over 100 chars) - - MCP OAuth Callback: Fixed callback page hanging and added multilingual support (10 languages) - - Error Display: Improved error block display order for better readability - - Plugin Browser: Centered tab alignment for better visual consistency + ✨ New Features: + - AI Providers: Added support for Hugging Face, Mistral, Perplexity, and SophNet + - Knowledge Base: OpenMinerU document preprocessor, full-text search in notes, enhanced tool selection + - Image & OCR: Intel OVMS painting provider and Intel OpenVINO (NPU) OCR support + - MCP Management: Redesigned interface with dual-column layout for easier management + - Languages: Added German language support - Bug Fixes: - - Fixed Agent sessions not inheriting allowed_tools configuration - - Fixed Gemini endpoint thinking budget spelling error - - Fixed MCP card description text overflow - - Fixed unnecessary message timestamp updates on UI-only state changes - - Updated dependencies: Bun to 1.3.1, uv to 0.9.5 + ⚡ Improvements: + - Upgraded to Electron 38.7.0 + - Enhanced system shutdown handling and automatic update checks + - Improved proxy bypass rules + + 🐛 Important Bug Fixes: + - Fixed streaming response issues across multiple AI providers + - Fixed session list scrolling problems + - Fixed knowledge base deletion errors - v1.7.0-beta.5 新特性 + v1.7.0-rc.1 新特性 - 新功能: - - MCPRouter 提供商:新增 MCPRouter 提供商集成,支持 token 管理和服务器同步 - - MCP 市场:增强 MCP 服务器发现和管理功能,支持多提供商市场 - - Agent 权限模式展示:空会话状态显示可视化权限模式卡片 - - 助手订阅设置:在助手预设中添加订阅 URL 管理功能 + 🎉 重大更新:AI Agent 智能体系统 + - 创建和管理专属 AI Agent,配置专用工具和权限 + - 独立的 Agent 会话,使用 SQLite 持久化存储,与普通聊天分离 + - 实时工具审批系统 - 动态审查和批准 Agent 操作 + - MCP(模型上下文协议)集成,连接外部工具 + - 支持斜杠命令快速交互 + - 兼容 OpenAI 的 REST API 访问 - 改进: - - UI 优化:macOS 上侧边栏工具提示位置优化,避免与窗口控制按钮重叠 - - MCP 服务器标志:在 Agent 设置工具部分显示服务器 logo - - 长命令处理:Bash 命令标签自动截断(超过 100 字符时悬停查看完整内容) - - MCP OAuth 回调:修复回调页面挂起问题并添加多语言支持(10 种语言) - - 错误信息展示:改进错误块显示顺序,提高可读性 - - 插件浏览器:标签页居中对齐,视觉效果更统一 + ✨ 新功能: + - AI 提供商:新增 Hugging Face、Mistral、Perplexity 和 SophNet 支持 + - 知识库:OpenMinerU 文档预处理器、笔记全文搜索、增强的工具选择 + - 图像与 OCR:Intel OVMS 绘图提供商和 Intel OpenVINO (NPU) OCR 支持 + - MCP 管理:重构管理界面,采用双列布局,更加方便管理 + - 语言:新增德语支持 - 问题修复: - - 修复 Agent 会话未继承 allowed_tools 配置 - - 修复 Gemini 端点 thinking budget 拼写错误 - - 修复 MCP 卡片描述文本溢出问题 - - 修复仅 UI 状态变化时消息时间戳不必要的更新 - - 依赖更新:Bun 升级到 1.3.1,uv 升级到 0.9.5 + ⚡ 改进: + - 升级到 Electron 38.7.0 + - 增强的系统关机处理和自动更新检查 + - 改进的代理绕过规则 + + 🐛 重要修复: + - 修复多个 AI 提供商的流式响应问题 + - 修复会话列表滚动问题 + - 修复知识库删除错误 diff --git a/package.json b/package.json index ea6da84ff8..3a302a5452 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts", "auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts", "update:languages": "tsx scripts/update-languages.ts", + "update:upgrade-config": "tsx scripts/update-app-upgrade-config.ts", "test": "vitest run --silent", "test:main": "vitest run --project main", "test:renderer": "vitest run --project renderer", @@ -110,6 +111,8 @@ "@agentic/searxng": "^7.3.3", "@agentic/tavily": "^7.3.3", "@ai-sdk/amazon-bedrock": "^3.0.53", + "@ai-sdk/cerebras": "^1.0.31", + "@ai-sdk/gateway": "^2.0.9", "@ai-sdk/google-vertex": "^3.0.62", "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch", "@ai-sdk/mistral": "^2.0.23", @@ -260,12 +263,12 @@ "dotenv-cli": "^7.4.2", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.5", - "electron": "38.4.0", - "electron-builder": "26.0.15", + "electron": "38.7.0", + "electron-builder": "26.1.0", "electron-devtools-installer": "^3.2.0", "electron-reload": "^2.0.0-alpha.1", "electron-store": "^8.2.0", - "electron-updater": "6.6.4", + "electron-updater": "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch", "electron-vite": "4.0.1", "electron-window-state": "^5.0.3", "emittery": "^1.0.3", @@ -382,13 +385,11 @@ "@codemirror/lint": "6.8.5", "@codemirror/view": "6.38.1", "@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", - "app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch", - "app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch", "atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch", "esbuild": "^0.25.0", "file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch", "libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch", - "node-abi": "4.12.0", + "node-abi": "4.24.0", "openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.5.0", "openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.5.0", "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index ed46c4f5f1..caeb1ae8db 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -197,12 +197,22 @@ export enum FeedUrl { GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download' } +export enum UpdateConfigUrl { + GITHUB = 'https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/x-files/app-upgrade-config/app-upgrade-config.json', + GITCODE = 'https://raw.gitcode.com/CherryHQ/cherry-studio/raw/x-files/app-upgrade-config/app-upgrade-config.json' +} + // export enum UpgradeChannel { // LATEST = 'latest', // 最新稳定版本 // RC = 'rc', // 公测版本 // BETA = 'beta' // 预览版本 // } +export enum UpdateMirror { + GITHUB = 'github', + GITCODE = 'gitcode' +} + export const defaultTimeout = 10 * 1000 * 60 export const occupiedDirs = ['logs', 'Network', 'Partitions/webview/Network'] diff --git a/scripts/update-app-upgrade-config.ts b/scripts/update-app-upgrade-config.ts new file mode 100644 index 0000000000..4fcfa647f7 --- /dev/null +++ b/scripts/update-app-upgrade-config.ts @@ -0,0 +1,532 @@ +import fs from 'fs/promises' +import path from 'path' +import semver from 'semver' + +type UpgradeChannel = 'latest' | 'rc' | 'beta' +type UpdateMirror = 'github' | 'gitcode' + +const CHANNELS: UpgradeChannel[] = ['latest', 'rc', 'beta'] +const MIRRORS: UpdateMirror[] = ['github', 'gitcode'] +const GITHUB_REPO = 'CherryHQ/cherry-studio' +const GITCODE_REPO = 'CherryHQ/cherry-studio' +const DEFAULT_FEED_TEMPLATES: Record = { + github: `https://github.com/${GITHUB_REPO}/releases/download/{{tag}}`, + gitcode: `https://gitcode.com/${GITCODE_REPO}/releases/download/{{tag}}` +} +const GITCODE_LATEST_FALLBACK = 'https://releases.cherry-ai.com' + +interface CliOptions { + tag?: string + configPath?: string + segmentsPath?: string + dryRun?: boolean + skipReleaseChecks?: boolean + isPrerelease?: boolean +} + +interface ChannelTemplateConfig { + feedTemplates?: Partial> +} + +interface SegmentMatchRule { + range?: string + exact?: string[] + excludeExact?: string[] +} + +interface SegmentDefinition { + id: string + type: 'legacy' | 'breaking' | 'latest' + match: SegmentMatchRule + lockedVersion?: string + minCompatibleVersion: string + description: string + channelTemplates?: Partial> +} + +interface SegmentMetadataFile { + segments: SegmentDefinition[] +} + +interface ChannelConfig { + version: string + feedUrls: Record +} + +interface VersionMetadata { + segmentId: string + segmentType?: string +} + +interface VersionEntry { + metadata?: VersionMetadata + minCompatibleVersion: string + description: string + channels: Record +} + +interface UpgradeConfigFile { + lastUpdated: string + versions: Record +} + +interface ReleaseInfo { + tag: string + version: string + channel: UpgradeChannel +} + +interface UpdateVersionsResult { + versions: Record + updated: boolean +} + +const ROOT_DIR = path.resolve(__dirname, '..') +const DEFAULT_CONFIG_PATH = path.join(ROOT_DIR, 'app-upgrade-config.json') +const DEFAULT_SEGMENTS_PATH = path.join(ROOT_DIR, 'config/app-upgrade-segments.json') + +async function main() { + const options = parseArgs() + const releaseTag = resolveTag(options) + const normalizedVersion = normalizeVersion(releaseTag) + const releaseChannel = detectChannel(normalizedVersion) + if (!releaseChannel) { + console.warn(`[update-app-upgrade-config] Tag ${normalizedVersion} does not map to beta/rc/latest. Skipping.`) + return + } + + // Validate version format matches prerelease status + if (options.isPrerelease !== undefined) { + const hasPrereleaseSuffix = releaseChannel === 'beta' || releaseChannel === 'rc' + + if (options.isPrerelease && !hasPrereleaseSuffix) { + console.warn( + `[update-app-upgrade-config] ⚠️ Release marked as prerelease but version ${normalizedVersion} has no beta/rc suffix. Skipping.` + ) + return + } + + if (!options.isPrerelease && hasPrereleaseSuffix) { + console.warn( + `[update-app-upgrade-config] ⚠️ Release marked as latest but version ${normalizedVersion} has prerelease suffix (${releaseChannel}). Skipping.` + ) + return + } + } + + const [config, segmentFile] = await Promise.all([ + readJson(options.configPath ?? DEFAULT_CONFIG_PATH), + readJson(options.segmentsPath ?? DEFAULT_SEGMENTS_PATH) + ]) + + const segment = pickSegment(segmentFile.segments, normalizedVersion) + if (!segment) { + throw new Error(`Unable to find upgrade segment for version ${normalizedVersion}`) + } + + if (segment.lockedVersion && segment.lockedVersion !== normalizedVersion) { + throw new Error(`Segment ${segment.id} is locked to ${segment.lockedVersion}, but received ${normalizedVersion}`) + } + + const releaseInfo: ReleaseInfo = { + tag: formatTag(releaseTag), + version: normalizedVersion, + channel: releaseChannel + } + + const { versions: updatedVersions, updated } = await updateVersions( + config.versions, + segment, + releaseInfo, + Boolean(options.skipReleaseChecks) + ) + + if (!updated) { + throw new Error( + `[update-app-upgrade-config] Feed URLs are not ready for ${releaseInfo.version} (${releaseInfo.channel}). Try again after the release mirrors finish syncing.` + ) + } + + const updatedConfig: UpgradeConfigFile = { + ...config, + lastUpdated: new Date().toISOString(), + versions: updatedVersions + } + + const output = JSON.stringify(updatedConfig, null, 2) + '\n' + + if (options.dryRun) { + console.log('Dry run enabled. Generated configuration:\n') + console.log(output) + return + } + + await fs.writeFile(options.configPath ?? DEFAULT_CONFIG_PATH, output, 'utf-8') + console.log( + `✅ Updated ${path.relative(process.cwd(), options.configPath ?? DEFAULT_CONFIG_PATH)} for ${segment.id} (${releaseInfo.channel}) -> ${releaseInfo.version}` + ) +} + +function parseArgs(): CliOptions { + const args = process.argv.slice(2) + const options: CliOptions = {} + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i] + if (arg === '--tag') { + options.tag = args[i + 1] + i += 1 + } else if (arg === '--config') { + options.configPath = args[i + 1] + i += 1 + } else if (arg === '--segments') { + options.segmentsPath = args[i + 1] + i += 1 + } else if (arg === '--dry-run') { + options.dryRun = true + } else if (arg === '--skip-release-checks') { + options.skipReleaseChecks = true + } else if (arg === '--is-prerelease') { + options.isPrerelease = args[i + 1] === 'true' + i += 1 + } else if (arg === '--help') { + printHelp() + process.exit(0) + } else { + console.warn(`Ignoring unknown argument "${arg}"`) + } + } + + if (options.skipReleaseChecks && !options.dryRun) { + throw new Error('--skip-release-checks can only be used together with --dry-run') + } + + return options +} + +function printHelp() { + console.log(`Usage: tsx scripts/update-app-upgrade-config.ts [options] + +Options: + --tag Release tag (e.g. v2.1.6). Falls back to GITHUB_REF_NAME/RELEASE_TAG. + --config Path to app-upgrade-config.json. + --segments Path to app-upgrade-segments.json. + --is-prerelease Whether this is a prerelease (validates version format). + --dry-run Print the result without writing to disk. + --skip-release-checks Skip release page availability checks (only valid with --dry-run). + --help Show this help message.`) +} + +function resolveTag(options: CliOptions): string { + const envTag = process.env.RELEASE_TAG ?? process.env.GITHUB_REF_NAME ?? process.env.TAG_NAME + const tag = options.tag ?? envTag + + if (!tag) { + throw new Error('A release tag is required. Pass --tag or set RELEASE_TAG/GITHUB_REF_NAME.') + } + + return tag +} + +function normalizeVersion(tag: string): string { + const cleaned = semver.clean(tag, { loose: true }) + if (!cleaned) { + throw new Error(`Tag "${tag}" is not a valid semantic version`) + } + + const valid = semver.valid(cleaned, { loose: true }) + if (!valid) { + throw new Error(`Unable to normalize tag "${tag}" to a valid semantic version`) + } + + return valid +} + +function detectChannel(version: string): UpgradeChannel | null { + const parsed = semver.parse(version, { loose: true, includePrerelease: true }) + if (!parsed) { + return null + } + + if (parsed.prerelease.length === 0) { + return 'latest' + } + + const label = String(parsed.prerelease[0]).toLowerCase() + if (label === 'beta') { + return 'beta' + } + if (label === 'rc') { + return 'rc' + } + + return null +} + +async function readJson(filePath: string): Promise { + const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(filePath) + const data = await fs.readFile(absolute, 'utf-8') + return JSON.parse(data) as T +} + +function pickSegment(segments: SegmentDefinition[], version: string): SegmentDefinition | null { + for (const segment of segments) { + if (matchesSegment(segment.match, version)) { + return segment + } + } + return null +} + +function matchesSegment(matchRule: SegmentMatchRule, version: string): boolean { + if (matchRule.exact && matchRule.exact.includes(version)) { + return true + } + + if (matchRule.excludeExact && matchRule.excludeExact.includes(version)) { + return false + } + + if (matchRule.range && !semver.satisfies(version, matchRule.range, { includePrerelease: true })) { + return false + } + + if (matchRule.exact) { + return matchRule.exact.includes(version) + } + + return Boolean(matchRule.range) +} + +function formatTag(tag: string): string { + if (tag.startsWith('refs/tags/')) { + return tag.replace('refs/tags/', '') + } + return tag +} + +async function updateVersions( + versions: Record, + segment: SegmentDefinition, + releaseInfo: ReleaseInfo, + skipReleaseValidation: boolean +): Promise { + const versionsCopy: Record = { ...versions } + const existingKey = findVersionKeyBySegment(versionsCopy, segment.id) + const targetKey = resolveVersionKey(existingKey, segment, releaseInfo) + const shouldRename = existingKey && existingKey !== targetKey + + let entry: VersionEntry + if (existingKey) { + entry = { ...versionsCopy[existingKey], channels: { ...versionsCopy[existingKey].channels } } + } else { + entry = createEmptyVersionEntry() + } + + entry.channels = ensureChannelSlots(entry.channels) + + const channelUpdated = await applyChannelUpdate(entry, segment, releaseInfo, skipReleaseValidation) + if (!channelUpdated) { + return { versions, updated: false } + } + + if (shouldRename && existingKey) { + delete versionsCopy[existingKey] + } + + entry.metadata = { + segmentId: segment.id, + segmentType: segment.type + } + entry.minCompatibleVersion = segment.minCompatibleVersion + entry.description = segment.description + + versionsCopy[targetKey] = entry + return { + versions: sortVersionMap(versionsCopy), + updated: true + } +} + +function findVersionKeyBySegment(versions: Record, segmentId: string): string | null { + for (const [key, value] of Object.entries(versions)) { + if (value.metadata?.segmentId === segmentId) { + return key + } + } + return null +} + +function resolveVersionKey(existingKey: string | null, segment: SegmentDefinition, releaseInfo: ReleaseInfo): string { + if (segment.lockedVersion) { + return segment.lockedVersion + } + + if (releaseInfo.channel === 'latest') { + return releaseInfo.version + } + + if (existingKey) { + return existingKey + } + + const baseVersion = getBaseVersion(releaseInfo.version) + return baseVersion ?? releaseInfo.version +} + +function getBaseVersion(version: string): string | null { + const parsed = semver.parse(version, { loose: true, includePrerelease: true }) + if (!parsed) { + return null + } + return `${parsed.major}.${parsed.minor}.${parsed.patch}` +} + +function createEmptyVersionEntry(): VersionEntry { + return { + minCompatibleVersion: '', + description: '', + channels: { + latest: null, + rc: null, + beta: null + } + } +} + +function ensureChannelSlots( + channels: Record +): Record { + return CHANNELS.reduce( + (acc, channel) => { + acc[channel] = channels[channel] ?? null + return acc + }, + {} as Record + ) +} + +async function applyChannelUpdate( + entry: VersionEntry, + segment: SegmentDefinition, + releaseInfo: ReleaseInfo, + skipReleaseValidation: boolean +): Promise { + if (!CHANNELS.includes(releaseInfo.channel)) { + throw new Error(`Unsupported channel "${releaseInfo.channel}"`) + } + + const feedUrls = buildFeedUrls(segment, releaseInfo) + + if (skipReleaseValidation) { + console.warn( + `[update-app-upgrade-config] Skipping release availability validation for ${releaseInfo.version} (${releaseInfo.channel}).` + ) + } else { + const availability = await ensureReleaseAvailability(releaseInfo) + if (!availability.github) { + return false + } + if (releaseInfo.channel === 'latest' && !availability.gitcode) { + console.warn( + `[update-app-upgrade-config] gitcode release page not ready for ${releaseInfo.tag}. Falling back to ${GITCODE_LATEST_FALLBACK}.` + ) + feedUrls.gitcode = GITCODE_LATEST_FALLBACK + } + } + + entry.channels[releaseInfo.channel] = { + version: releaseInfo.version, + feedUrls + } + + return true +} + +function buildFeedUrls(segment: SegmentDefinition, releaseInfo: ReleaseInfo): Record { + return MIRRORS.reduce( + (acc, mirror) => { + const template = resolveFeedTemplate(segment, releaseInfo, mirror) + acc[mirror] = applyTemplate(template, releaseInfo) + return acc + }, + {} as Record + ) +} + +function resolveFeedTemplate(segment: SegmentDefinition, releaseInfo: ReleaseInfo, mirror: UpdateMirror): string { + if (mirror === 'gitcode' && releaseInfo.channel !== 'latest') { + return segment.channelTemplates?.[releaseInfo.channel]?.feedTemplates?.github ?? DEFAULT_FEED_TEMPLATES.github + } + + return segment.channelTemplates?.[releaseInfo.channel]?.feedTemplates?.[mirror] ?? DEFAULT_FEED_TEMPLATES[mirror] +} + +function applyTemplate(template: string, releaseInfo: ReleaseInfo): string { + return template.replace(/{{\s*tag\s*}}/gi, releaseInfo.tag).replace(/{{\s*version\s*}}/gi, releaseInfo.version) +} + +function sortVersionMap(versions: Record): Record { + const sorted = Object.entries(versions).sort(([a], [b]) => semver.rcompare(a, b)) + return sorted.reduce( + (acc, [version, entry]) => { + acc[version] = entry + return acc + }, + {} as Record + ) +} + +interface ReleaseAvailability { + github: boolean + gitcode: boolean +} + +async function ensureReleaseAvailability(releaseInfo: ReleaseInfo): Promise { + const mirrorsToCheck: UpdateMirror[] = releaseInfo.channel === 'latest' ? MIRRORS : ['github'] + const availability: ReleaseAvailability = { + github: false, + gitcode: releaseInfo.channel === 'latest' ? false : true + } + + for (const mirror of mirrorsToCheck) { + const url = getReleasePageUrl(mirror, releaseInfo.tag) + try { + const response = await fetch(url, { + method: mirror === 'github' ? 'HEAD' : 'GET', + redirect: 'follow' + }) + + if (response.ok) { + availability[mirror] = true + } else { + console.warn( + `[update-app-upgrade-config] ${mirror} release not available for ${releaseInfo.tag} (status ${response.status}, ${url}).` + ) + availability[mirror] = false + } + } catch (error) { + console.warn( + `[update-app-upgrade-config] Failed to verify ${mirror} release page for ${releaseInfo.tag} (${url}). Continuing.`, + error + ) + availability[mirror] = false + } + } + + return availability +} + +function getReleasePageUrl(mirror: UpdateMirror, tag: string): string { + if (mirror === 'github') { + return `https://github.com/${GITHUB_REPO}/releases/tag/${encodeURIComponent(tag)}` + } + // Use latest.yml download URL for GitCode to check if release exists + // Note: GitCode returns 401 for HEAD requests, so we use GET in ensureReleaseAvailability + return `https://gitcode.com/${GITCODE_REPO}/releases/download/${encodeURIComponent(tag)}/latest.yml` +} + +main().catch((error) => { + console.error('❌ Failed to update app-upgrade-config:', error) + process.exit(1) +}) diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index d63d7ed60c..0881bf8504 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -3,7 +3,7 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' import { getIpCountry } from '@main/utils/ipService' import { generateUserAgent, getClientId } from '@main/utils/systemInfo' -import { FeedUrl } from '@shared/config/constant' +import { FeedUrl, UpdateConfigUrl, UpdateMirror } from '@shared/config/constant' import { UpgradeChannel } from '@shared/data/preference/preferenceTypes' import { IpcChannel } from '@shared/IpcChannel' import type { UpdateInfo } from 'builder-util-runtime' @@ -23,7 +23,29 @@ const LANG_MARKERS = { EN_START: '', ZH_CN_START: '', END: '' -} as const +} + +interface UpdateConfig { + lastUpdated: string + versions: { + [versionKey: string]: VersionConfig + } +} + +interface VersionConfig { + minCompatibleVersion: string + description: string + channels: { + latest: ChannelConfig | null + rc: ChannelConfig | null + beta: ChannelConfig | null + } +} + +interface ChannelConfig { + version: string + feedUrls: Record +} export default class AppUpdater { autoUpdater: _AppUpdater = autoUpdater @@ -38,7 +60,9 @@ export default class AppUpdater { autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, 'User-Agent': generateUserAgent(), - 'X-Client-Id': getClientId() + 'X-Client-Id': getClientId(), + // no-cache + 'Cache-Control': 'no-cache' } autoUpdater.on('error', (error) => { @@ -76,61 +100,6 @@ export default class AppUpdater { this.autoUpdater = autoUpdater } - private async _getReleaseVersionFromGithub(channel: UpgradeChannel) { - const headers = { - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - 'Accept-Language': 'en-US,en;q=0.9' - } - try { - logger.info(`get release version from github: ${channel}`) - const responses = await net.fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', { - headers - }) - const data = (await responses.json()) as GithubReleaseInfo[] - let mightHaveLatest = false - const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => { - if (!item.draft && !item.prerelease) { - mightHaveLatest = true - } - - return item.prerelease && item.tag_name.includes(`-${channel}.`) - }) - - if (!release) { - return null - } - - // if the release version is the same as the current version, return null - if (release.tag_name === app.getVersion()) { - return null - } - - if (mightHaveLatest) { - logger.info(`might have latest release, get latest release`) - const latestReleaseResponse = await net.fetch( - 'https://api.github.com/repos/CherryHQ/cherry-studio/releases/latest', - { - headers - } - ) - const latestRelease = (await latestReleaseResponse.json()) as GithubReleaseInfo - if (semver.gt(latestRelease.tag_name, release.tag_name)) { - logger.info( - `latest release version is ${latestRelease.tag_name}, prerelease version is ${release.tag_name}, return null` - ) - return null - } - } - - logger.info(`release url is ${release.tag_name}, set channel to ${channel}`) - return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}` - } catch (error) { - logger.error('Failed to get latest not draft version from github:', error as Error) - return null - } - } - public setAutoUpdate(isActive: boolean) { autoUpdater.autoDownload = isActive autoUpdater.autoInstallOnAppQuit = isActive @@ -162,6 +131,88 @@ export default class AppUpdater { return UpgradeChannel.LATEST } + /** + * Fetch update configuration from GitHub or GitCode based on mirror + * @param mirror - Mirror to fetch config from + * @returns UpdateConfig object or null if fetch fails + */ + private async _fetchUpdateConfig(mirror: UpdateMirror): Promise { + const configUrl = mirror === UpdateMirror.GITCODE ? UpdateConfigUrl.GITCODE : UpdateConfigUrl.GITHUB + + try { + logger.info(`Fetching update config from ${configUrl} (mirror: ${mirror})`) + const response = await net.fetch(configUrl, { + headers: { + 'User-Agent': generateUserAgent(), + Accept: 'application/json', + 'X-Client-Id': getClientId(), + // no-cache + 'Cache-Control': 'no-cache' + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const config = (await response.json()) as UpdateConfig + logger.info(`Update config fetched successfully, last updated: ${config.lastUpdated}`) + return config + } catch (error) { + logger.error('Failed to fetch update config:', error as Error) + return null + } + } + + /** + * Find compatible channel configuration based on current version + * @param currentVersion - Current app version + * @param requestedChannel - Requested upgrade channel (latest/rc/beta) + * @param config - Update configuration object + * @returns Object containing ChannelConfig and actual channel if found, null otherwise + */ + private _findCompatibleChannel( + currentVersion: string, + requestedChannel: UpgradeChannel, + config: UpdateConfig + ): { config: ChannelConfig; channel: UpgradeChannel } | null { + // Get all version keys and sort descending (newest first) + const versionKeys = Object.keys(config.versions).sort(semver.rcompare) + + logger.info( + `Finding compatible channel for version ${currentVersion}, requested channel: ${requestedChannel}, available versions: ${versionKeys.join(', ')}` + ) + + for (const versionKey of versionKeys) { + const versionConfig = config.versions[versionKey] + const channelConfig = versionConfig.channels[requestedChannel] + const latestChannelConfig = versionConfig.channels[UpgradeChannel.LATEST] + + // Check version compatibility and channel availability + if (semver.gte(currentVersion, versionConfig.minCompatibleVersion) && channelConfig !== null) { + logger.info( + `Found compatible version: ${versionKey} (minCompatibleVersion: ${versionConfig.minCompatibleVersion}), version: ${channelConfig.version}` + ) + + if ( + requestedChannel !== UpgradeChannel.LATEST && + latestChannelConfig && + semver.gte(latestChannelConfig.version, channelConfig.version) + ) { + logger.info( + `latest channel version is greater than the requested channel version: ${latestChannelConfig.version} > ${channelConfig.version}, using latest instead` + ) + return { config: latestChannelConfig, channel: UpgradeChannel.LATEST } + } + + return { config: channelConfig, channel: requestedChannel } + } + } + + logger.warn(`No compatible channel found for version ${currentVersion} and channel ${requestedChannel}`) + return null + } + private _setChannel(channel: UpgradeChannel, feedUrl: string) { this.autoUpdater.channel = channel this.autoUpdater.setFeedURL(feedUrl) @@ -173,33 +224,42 @@ export default class AppUpdater { } private async _setFeedUrl() { + const currentVersion = app.getVersion() const testPlan = preferenceService.get('app.dist.test_plan.enabled') - if (testPlan) { - const channel = this._getTestChannel() + const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST - if (channel === UpgradeChannel.LATEST) { - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) - return - } - - const releaseUrl = await this._getReleaseVersionFromGithub(channel) - if (releaseUrl) { - logger.info(`release url is ${releaseUrl}, set channel to ${channel}`) - this._setChannel(channel, releaseUrl) - return - } - - // if no prerelease url, use github latest to get release - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) - return - } - - this._setChannel(UpgradeChannel.LATEST, FeedUrl.PRODUCTION) + // Determine mirror based on IP country const ipCountry = await getIpCountry() - logger.info(`ipCountry is ${ipCountry}, set channel to ${UpgradeChannel.LATEST}`) - if (ipCountry.toLowerCase() !== 'cn') { - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) + const mirror = ipCountry.toLowerCase() === 'cn' ? UpdateMirror.GITCODE : UpdateMirror.GITHUB + + logger.info( + `Setting feed URL for version ${currentVersion}, testPlan: ${testPlan}, requested channel: ${requestedChannel}, mirror: ${mirror} (IP country: ${ipCountry})` + ) + + // Try to fetch update config from remote + const config = await this._fetchUpdateConfig(mirror) + + if (config) { + // Use new config-based system + const result = this._findCompatibleChannel(currentVersion, requestedChannel, config) + + if (result) { + const { config: channelConfig, channel: actualChannel } = result + const feedUrl = channelConfig.feedUrls[mirror] + logger.info( + `Using config-based feed URL: ${feedUrl} for channel ${actualChannel} (requested: ${requestedChannel}, mirror: ${mirror})` + ) + this._setChannel(actualChannel, feedUrl) + return + } } + + logger.info('Failed to fetch update config, falling back to default feed URL') + // Fallback: use default feed URL based on mirror + const defaultFeedUrl = mirror === UpdateMirror.GITCODE ? FeedUrl.PRODUCTION : FeedUrl.GITHUB_LATEST + + logger.info(`Using fallback feed URL: ${defaultFeedUrl}`) + this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl) } public cancelDownload() { @@ -321,8 +381,3 @@ export default class AppUpdater { return processedInfo } } -interface GithubReleaseInfo { - draft: boolean - prerelease: boolean - tag_name: string -} diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index ceb977a136..c21d336d5c 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -375,13 +375,16 @@ export class WindowService { mainWindow.hide() - // TODO: don't hide dock icon when close to tray - // will cause the cmd+h behavior not working - // after the electron fix the bug, we can restore this code - // //for mac users, should hide dock icon if close to tray - // if (isMac && isTrayOnClose) { - // app.dock?.hide() - // } + //for mac users, should hide dock icon if close to tray + if (isMac && isTrayOnClose) { + app.dock?.hide() + + mainWindow.once('show', () => { + //restore the window can hide by cmd+h when the window is shown again + // https://github.com/electron/electron/pull/47970 + app.dock?.show() + }) + } }) mainWindow.on('closed', () => { diff --git a/src/main/services/__tests__/AppUpdater.test.ts b/src/main/services/__tests__/AppUpdater.test.ts index 80a9b252f3..babc76ca81 100644 --- a/src/main/services/__tests__/AppUpdater.test.ts +++ b/src/main/services/__tests__/AppUpdater.test.ts @@ -83,6 +83,8 @@ vi.mock('electron-updater', () => ({ // Import after mocks import { preferenceService } from '@data/PreferenceService' +import { UpdateMirror } from '@shared/config/constant' +import { app, net } from 'electron' import { MockMainPreferenceServiceUtils } from '../../../../tests/__mocks__/main/PreferenceService' import AppUpdater from '../AppUpdater' @@ -284,4 +286,711 @@ describe('AppUpdater', () => { expect(result.releaseNotes).toBeNull() }) }) + + describe('_fetchUpdateConfig', () => { + const mockConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'Test version', + channels: { + latest: { + version: '1.6.7', + feedUrls: { + github: 'https://github.com/test/v1.6.7', + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: null, + beta: null + } + } + } + } + + it('should fetch config from GitHub mirror', async () => { + vi.mocked(net.fetch).mockResolvedValue({ + ok: true, + json: async () => mockConfig + } as any) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITHUB) + + expect(result).toEqual(mockConfig) + expect(net.fetch).toHaveBeenCalledWith(expect.stringContaining('github'), expect.any(Object)) + }) + + it('should fetch config from GitCode mirror', async () => { + vi.mocked(net.fetch).mockResolvedValue({ + ok: true, + json: async () => mockConfig + } as any) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITCODE) + + expect(result).toEqual(mockConfig) + // GitCode URL may vary, just check that fetch was called + expect(net.fetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + }) + + it('should return null on HTTP error', async () => { + vi.mocked(net.fetch).mockResolvedValue({ + ok: false, + status: 404 + } as any) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITHUB) + + expect(result).toBeNull() + }) + + it('should return null on network error', async () => { + vi.mocked(net.fetch).mockRejectedValue(new Error('Network error')) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITHUB) + + expect(result).toBeNull() + }) + }) + + describe('_findCompatibleChannel', () => { + const mockConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'v1.6.7', + channels: { + latest: { + version: '1.6.7', + feedUrls: { + github: 'https://github.com/test/v1.6.7', + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: { + version: '1.7.0-rc.1', + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: { + version: '1.7.0-beta.3', + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.3', + gitcode: 'https://gitcode.com/test/v1.7.0-beta.3' + } + } + } + }, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'v2.0.0', + channels: { + latest: null, + rc: null, + beta: null + } + } + } + } + + it('should find compatible latest channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.5.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'latest', mockConfig) + + expect(result?.config).toEqual({ + version: '1.6.7', + feedUrls: { + github: 'https://github.com/test/v1.6.7', + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + expect(result?.channel).toBe('latest') + }) + + it('should find compatible rc channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.5.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'rc', mockConfig) + + expect(result?.config).toEqual({ + version: '1.7.0-rc.1', + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }) + expect(result?.channel).toBe('rc') + }) + + it('should find compatible beta channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.5.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'beta', mockConfig) + + expect(result?.config).toEqual({ + version: '1.7.0-beta.3', + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.3', + gitcode: 'https://gitcode.com/test/v1.7.0-beta.3' + } + }) + expect(result?.channel).toBe('beta') + }) + + it('should return latest when latest version >= rc version', () => { + const configWithNewerLatest = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.7.0', + feedUrls: { + github: 'https://github.com/test/v1.7.0', + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }, + rc: { + version: '1.7.0-rc.1', + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'rc', configWithNewerLatest) + + // Should return latest instead of rc because 1.7.0 >= 1.7.0-rc.1 + expect(result?.config).toEqual({ + version: '1.7.0', + feedUrls: { + github: 'https://github.com/test/v1.7.0', + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }) + expect(result?.channel).toBe('latest') // ✅ 返回 latest 频道 + }) + + it('should return latest when latest version >= beta version', () => { + const configWithNewerLatest = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }, + rc: null, + beta: { + version: '1.6.8-beta.1', + + feedUrls: { + github: 'https://github.com/test/v1.6.8-beta.1', + + gitcode: 'https://gitcode.com/test/v1.6.8-beta.1' + } + } + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'beta', configWithNewerLatest) + + // Should return latest instead of beta because 1.7.0 >= 1.6.8-beta.1 + expect(result?.config).toEqual({ + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }) + }) + + it('should not compare latest with itself when requesting latest channel', () => { + const config = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }, + rc: { + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'latest', config) + + // Should return latest directly without comparing with itself + expect(result?.config).toEqual({ + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }) + }) + + it('should return rc when rc version > latest version', () => { + const configWithNewerRc = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: { + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'rc', configWithNewerRc) + + // Should return rc because 1.7.0-rc.1 > 1.6.7 + expect(result?.config).toEqual({ + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }) + }) + + it('should return beta when beta version > latest version', () => { + const configWithNewerBeta = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: null, + beta: { + version: '1.7.0-beta.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.5', + + gitcode: 'https://gitcode.com/test/v1.7.0-beta.5' + } + } + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'beta', configWithNewerBeta) + + // Should return beta because 1.7.0-beta.5 > 1.6.7 + expect(result?.config).toEqual({ + version: '1.7.0-beta.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.5', + + gitcode: 'https://gitcode.com/test/v1.7.0-beta.5' + } + }) + }) + + it('should return lower version when higher version has no compatible channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.8.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.8.0', 'latest', mockConfig) + + // 1.8.0 >= 1.7.0 but 2.0.0 has no latest channel, so return 1.6.7 + expect(result?.config).toEqual({ + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + }) + + it('should return null when current version does not meet minCompatibleVersion', () => { + vi.mocked(app.getVersion).mockReturnValue('0.9.0') + + const result = (appUpdater as any)._findCompatibleChannel('0.9.0', 'latest', mockConfig) + + // 0.9.0 < 1.0.0 (minCompatibleVersion) + expect(result).toBeNull() + }) + + it('should return lower version rc when higher version has no rc channel', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.8.0', 'rc', mockConfig) + + // 1.8.0 >= 1.7.0 but 2.0.0 has no rc channel, so return 1.6.7 rc + expect(result?.config).toEqual({ + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }) + }) + + it('should return null when no version has the requested channel', () => { + const configWithoutRc = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'v1.6.7', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: null, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'rc', configWithoutRc) + + expect(result).toBeNull() + }) + }) + + describe('Upgrade Path', () => { + const fullConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'Last v1.x', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: { + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: { + version: '1.7.0-beta.3', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.3', + + gitcode: 'https://gitcode.com/test/v1.7.0-beta.3' + } + } + } + }, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'First v2.x', + channels: { + latest: null, + rc: null, + beta: null + } + } + } + } + + it('should upgrade from 1.6.3 to 1.6.7', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.3', 'latest', fullConfig) + + expect(result?.config).toEqual({ + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + }) + + it('should block upgrade from 1.6.7 to 2.0.0 (minCompatibleVersion not met)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.7', 'latest', fullConfig) + + // Should return 1.6.7, not 2.0.0, because 1.6.7 < 1.7.0 (minCompatibleVersion of 2.0.0) + expect(result?.config).toEqual({ + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + }) + + it('should allow upgrade from 1.7.0 to 2.0.0', () => { + const configWith2x = { + ...fullConfig, + versions: { + ...fullConfig.versions, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'First v2.x', + channels: { + latest: { + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }, + rc: null, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.7.0', 'latest', configWith2x) + + expect(result?.config).toEqual({ + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }) + }) + }) + + describe('Complete Multi-Step Upgrade Path', () => { + const fullUpgradeConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.5': { + minCompatibleVersion: '1.0.0', + description: 'Last v1.x stable', + channels: { + latest: { + version: '1.7.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.5', + + gitcode: 'https://gitcode.com/test/v1.7.5' + } + }, + rc: null, + beta: null + } + }, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'First v2.x - intermediate version', + channels: { + latest: { + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }, + rc: null, + beta: null + } + }, + '2.1.6': { + minCompatibleVersion: '2.0.0', + description: 'Current v2.x stable', + channels: { + latest: { + version: '2.1.6', + + feedUrls: { + github: 'https://github.com/test/latest', + + gitcode: 'https://gitcode.com/test/latest' + } + }, + rc: null, + beta: null + } + } + } + } + + it('should upgrade from 1.6.3 to 1.7.5 (step 1)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.3', 'latest', fullUpgradeConfig) + + expect(result?.config).toEqual({ + version: '1.7.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.5', + + gitcode: 'https://gitcode.com/test/v1.7.5' + } + }) + }) + + it('should upgrade from 1.7.5 to 2.0.0 (step 2)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.7.5', 'latest', fullUpgradeConfig) + + expect(result?.config).toEqual({ + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }) + }) + + it('should upgrade from 2.0.0 to 2.1.6 (step 3)', () => { + const result = (appUpdater as any)._findCompatibleChannel('2.0.0', 'latest', fullUpgradeConfig) + + expect(result?.config).toEqual({ + version: '2.1.6', + + feedUrls: { + github: 'https://github.com/test/latest', + + gitcode: 'https://gitcode.com/test/latest' + } + }) + }) + + it('should complete full upgrade path: 1.6.3 -> 1.7.5 -> 2.0.0 -> 2.1.6', () => { + // Step 1: 1.6.3 -> 1.7.5 + let currentVersion = '1.6.3' + let result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('1.7.5') + + // Step 2: 1.7.5 -> 2.0.0 + currentVersion = result?.config.version! + result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('2.0.0') + + // Step 3: 2.0.0 -> 2.1.6 + currentVersion = result?.config.version! + result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('2.1.6') + + // Final: 2.1.6 is the latest, no more upgrades + currentVersion = result?.config.version! + result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('2.1.6') + }) + + it('should block direct upgrade from 1.6.3 to 2.0.0 (skip intermediate)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.3', 'latest', fullUpgradeConfig) + + // Should return 1.7.5, not 2.0.0, because 1.6.3 < 1.7.0 (minCompatibleVersion of 2.0.0) + expect(result?.config.version).toBe('1.7.5') + expect(result?.config.version).not.toBe('2.0.0') + }) + + it('should block direct upgrade from 1.7.5 to 2.1.6 (skip intermediate)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.7.5', 'latest', fullUpgradeConfig) + + // Should return 2.0.0, not 2.1.6, because 1.7.5 < 2.0.0 (minCompatibleVersion of 2.1.6) + expect(result?.config.version).toBe('2.0.0') + expect(result?.config.version).not.toBe('2.1.6') + }) + }) }) diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index c77976c860..b4fa11529f 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -7,16 +7,17 @@ * 2. 暂时保持接口兼容性 */ +import type { GatewayLanguageModelEntry } from '@ai-sdk/gateway' import { createExecutor } from '@cherrystudio/ai-core' import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' import { addSpan, endSpan } from '@renderer/services/SpanManagerService' import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity' -import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types' +import { type Assistant, type GenerateImageParams, type Model, type Provider, SystemProviderIds } from '@renderer/types' import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes' import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils' import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic' -import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai' +import { gateway, type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai' import AiSdkToChunkAdapter from './chunk/AiSdkToChunkAdapter' import LegacyAiProvider from './legacy/index' @@ -439,6 +440,18 @@ export default class ModernAiProvider { // 代理其他方法到原有实现 public async models() { + if (this.actualProvider.id === SystemProviderIds['ai-gateway']) { + const formatModel = function (models: GatewayLanguageModelEntry[]): Model[] { + return models.map((m) => ({ + id: m.id, + name: m.name, + provider: 'gateway', + group: m.id.split('/')[0], + description: m.description ?? undefined + })) + } + return formatModel((await gateway.getAvailableModels()).models) + } return this.legacyProvider.models() } diff --git a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts index b9131be661..40cace50ea 100644 --- a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts @@ -90,7 +90,7 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< if (isOpenAILLMModel(model) && !isOpenAIChatCompletionOnlyModel(model)) { if (this.provider.id === 'azure-openai' || this.provider.type === 'azure-openai') { this.provider = { ...this.provider, apiHost: this.formatApiHost() } - if (this.provider.apiVersion === 'preview') { + if (this.provider.apiVersion === 'preview' || this.provider.apiVersion === 'v1') { return this } else { return this.client diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 84ecc8ffd9..094ab3de1e 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -190,9 +190,11 @@ export function providerToAiSdkConfig( } } // azure + // https://learn.microsoft.com/en-us/azure/ai-foundry/openai/latest + // https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/responses?tabs=python-key#responses-api if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') { - // extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1,不使用azure endpoint - if (actualProvider.apiVersion === 'preview') { + // extraOptions.apiVersion = actualProvider.apiVersion === 'preview' ? 'v1' : actualProvider.apiVersion 默认使用v1,不使用azure endpoint + if (actualProvider.apiVersion === 'preview' || actualProvider.apiVersion === 'v1') { extraOptions.mode = 'responses' } else { extraOptions.mode = 'chat' diff --git a/src/renderer/src/aiCore/provider/providerInitialization.ts b/src/renderer/src/aiCore/provider/providerInitialization.ts index 665f2bd05c..baf400508a 100644 --- a/src/renderer/src/aiCore/provider/providerInitialization.ts +++ b/src/renderer/src/aiCore/provider/providerInitialization.ts @@ -71,6 +71,21 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [ creatorFunctionName: 'createHuggingFace', supportsImageGeneration: true, aliases: ['hf', 'hugging-face'] + }, + { + id: 'ai-gateway', + name: 'AI Gateway', + import: () => import('@ai-sdk/gateway'), + creatorFunctionName: 'createGateway', + supportsImageGeneration: true, + aliases: ['gateway'] + }, + { + id: 'cerebras', + name: 'Cerebras', + import: () => import('@ai-sdk/cerebras'), + creatorFunctionName: 'createCerebras', + supportsImageGeneration: false } ] as const diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 9e296597c2..88f556438b 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -151,11 +151,12 @@ export function buildProviderOptions( ...providerSpecificOptions, ...getCustomParameters(assistant) } - // vertex需要映射到google或anthropic + const rawProviderKey = { 'google-vertex': 'google', - 'google-vertex-anthropic': 'anthropic' + 'google-vertex-anthropic': 'anthropic', + 'ai-gateway': 'gateway' }[rawProviderId] || rawProviderId // 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions } diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 1d7123a47b..d0b6f1df25 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -109,6 +109,11 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin // use thinking, doubao, zhipu, etc. if (isSupportedThinkingTokenDoubaoModel(model) || isSupportedThinkingTokenZhipuModel(model)) { + if (provider.id === SystemProviderIds.cerebras) { + return { + disable_reasoning: true + } + } return { thinking: { type: 'disabled' } } } @@ -306,6 +311,9 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin return {} } if (isSupportedThinkingTokenZhipuModel(model)) { + if (provider.id === SystemProviderIds.cerebras) { + return {} + } return { thinking: { type: 'enabled' } } } diff --git a/src/renderer/src/assets/images/providers/cerebras.webp b/src/renderer/src/assets/images/providers/cerebras.webp new file mode 100644 index 0000000000..1c21b6ff26 Binary files /dev/null and b/src/renderer/src/assets/images/providers/cerebras.webp differ diff --git a/src/renderer/src/assets/images/providers/vercel.svg b/src/renderer/src/assets/images/providers/vercel.svg new file mode 100644 index 0000000000..486cb95787 --- /dev/null +++ b/src/renderer/src/assets/images/providers/vercel.svg @@ -0,0 +1 @@ +Vercel \ No newline at end of file diff --git a/src/renderer/src/components/ContentSearch.tsx b/src/renderer/src/components/ContentSearch.tsx index 6eb4ea7b1e..ef1d31f7c6 100644 --- a/src/renderer/src/components/ContentSearch.tsx +++ b/src/renderer/src/components/ContentSearch.tsx @@ -1,6 +1,7 @@ import { Tooltip } from '@cherrystudio/ui' import { ActionIconButton } from '@renderer/components/Buttons' import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout' +import { scrollElementIntoView } from '@renderer/utils' import { debounce } from 'lodash' import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react' import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' @@ -181,17 +182,14 @@ export const ContentSearch = React.forwardRef( // 3. 将当前项滚动到视图中 // 获取第一个文本节点的父元素来进行滚动 const parentElement = currentMatchRange.startContainer.parentElement - if (shouldScroll) { - parentElement?.scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'nearest' - }) + if (shouldScroll && parentElement) { + // 优先在指定的滚动容器内滚动,避免滚动整个页面导致索引错乱/看起来"跳到第一条" + scrollElementIntoView(parentElement, target) } } } }, - [allRanges, currentIndex] + [allRanges, currentIndex, target] ) const search = useCallback( diff --git a/src/renderer/src/components/VirtualList/dynamic.tsx b/src/renderer/src/components/VirtualList/dynamic.tsx index 07fe5b1703..e2644fea35 100644 --- a/src/renderer/src/components/VirtualList/dynamic.tsx +++ b/src/renderer/src/components/VirtualList/dynamic.tsx @@ -81,6 +81,16 @@ export interface DynamicVirtualListProps extends InheritedVirtualizerOptions * Hide the scrollbar automatically when scrolling is stopped */ autoHideScrollbar?: boolean + + /** + * Header content to display above the list + */ + header?: React.ReactNode + + /** + * Additional CSS class name for the container + */ + className?: string } function DynamicVirtualList(props: DynamicVirtualListProps) { @@ -95,6 +105,8 @@ function DynamicVirtualList(props: DynamicVirtualListProps) { itemContainerStyle, scrollerStyle, autoHideScrollbar = false, + header, + className, ...restOptions } = props @@ -189,7 +201,7 @@ function DynamicVirtualList(props: DynamicVirtualListProps) { return ( (props: DynamicVirtualListProps) { ...(horizontal ? { width: size ?? '100%' } : { height: size ?? '100%' }), ...scrollerStyle }}> + {header}
= provider: 'minimax', name: 'minimax-01', group: 'minimax-01' + }, + { + id: 'MiniMax-M2', + provider: 'minimax', + name: 'MiniMax M2', + group: 'minimax-m2' + }, + { + id: 'MiniMax-M2-Stable', + provider: 'minimax', + name: 'MiniMax M2 Stable', + group: 'minimax-m2' } ], hyperbolic: [ @@ -1840,5 +1852,26 @@ export const SYSTEM_MODELS: Record = group: 'LongCat' } ], - huggingface: [] + huggingface: [], + 'ai-gateway': [], + cerebras: [ + { + id: 'gpt-oss-120b', + name: 'GPT oss 120B', + provider: 'cerebras', + group: 'openai' + }, + { + id: 'zai-glm-4.6', + name: 'GLM 4.6', + provider: 'cerebras', + group: 'zai' + }, + { + id: 'qwen-3-235b-a22b-instruct-2507', + name: 'Qwen 3 235B A22B Instruct', + provider: 'cerebras', + group: 'qwen' + } + ] } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 50b0dbaece..965c620ba9 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -12,6 +12,7 @@ import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-clou import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png' import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg' +import CerebrasProviderLogo from '@renderer/assets/images/providers/cerebras.webp' import CherryInProviderLogo from '@renderer/assets/images/providers/cherryin.png' import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png' import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png' @@ -51,6 +52,7 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png' import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png' import TogetherProviderLogo from '@renderer/assets/images/providers/together.png' import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' +import AIGatewayProviderLogo from '@renderer/assets/images/providers/vercel.svg' import VertexAIProviderLogo from '@renderer/assets/images/providers/vertexai.svg' import BytedanceProviderLogo from '@renderer/assets/images/providers/volcengine.png' import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png' @@ -470,7 +472,8 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = name: 'MiniMax', type: 'openai', apiKey: '', - apiHost: 'https://api.minimax.chat/v1/', + apiHost: 'https://api.minimaxi.com/v1', + anthropicApiHost: 'https://api.minimaxi.com/anthropic', models: SYSTEM_MODELS.minimax, isSystem: true, enabled: false @@ -675,6 +678,26 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = models: [], isSystem: true, enabled: false + }, + 'ai-gateway': { + id: 'ai-gateway', + name: 'AI Gateway', + type: 'ai-gateway', + apiKey: '', + apiHost: 'https://ai-gateway.vercel.sh/v1', + models: [], + isSystem: true, + enabled: false + }, + cerebras: { + id: 'cerebras', + name: 'Cerebras AI', + type: 'openai', + apiKey: '', + apiHost: 'https://api.cerebras.ai/v1', + models: SYSTEM_MODELS.cerebras, + isSystem: true, + enabled: false } } as const @@ -741,7 +764,9 @@ export const PROVIDER_LOGO_MAP: AtLeast = { aionly: AiOnlyProviderLogo, longcat: LongCatProviderLogo, huggingface: HuggingfaceProviderLogo, - sophnet: SophnetProviderLogo + sophnet: SophnetProviderLogo, + 'ai-gateway': AIGatewayProviderLogo, + cerebras: CerebrasProviderLogo } as const export function getProviderLogo(providerId: string) { @@ -1048,7 +1073,7 @@ export const PROVIDER_URLS: Record = { }, minimax: { api: { - url: 'https://api.minimax.chat/v1/' + url: 'https://api.minimaxi.com/v1/' }, websites: { official: 'https://platform.minimaxi.com/', @@ -1390,6 +1415,28 @@ export const PROVIDER_URLS: Record = { docs: 'https://huggingface.co/docs', models: 'https://huggingface.co/models' } + }, + 'ai-gateway': { + api: { + url: 'https://ai-gateway.vercel.sh/v1/ai' + }, + websites: { + official: 'https://vercel.com/ai-gateway', + apiKey: 'https://vercel.com/', + docs: 'https://vercel.com/docs/ai-gateway', + models: 'https://vercel.com/ai-gateway/models' + } + }, + cerebras: { + api: { + url: 'https://api.cerebras.ai/v1' + }, + websites: { + official: 'https://www.cerebras.ai', + apiKey: 'https://cloud.cerebras.ai', + docs: 'https://inference-docs.cerebras.ai/introduction', + models: 'https://inference-docs.cerebras.ai/models/overview' + } } } @@ -1452,7 +1499,7 @@ export const isSupportEnableThinkingProvider = (provider: Provider) => { ) } -const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[] +const NOT_SUPPORT_SERVICE_TIER_PROVIDERS = ['github', 'copilot', 'cerebras'] as const satisfies SystemProviderId[] /** * 判断提供商是否支持 service_tier 设置。 Only for OpenAI API. @@ -1519,6 +1566,10 @@ export function isGeminiProvider(provider: Provider): boolean { return provider.type === 'gemini' } +export function isAIGatewayProvider(provider: Provider): boolean { + return provider.type === 'ai-gateway' +} + const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot', 'perplexity'] as const satisfies SystemProviderId[] export const isSupportAPIVersionProvider = (provider: Provider) => { diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index f8806359d4..f657fd0e08 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -86,7 +86,9 @@ const providerKeyMap = { aionly: 'provider.aionly', longcat: 'provider.longcat', huggingface: 'provider.huggingface', - sophnet: 'provider.sophnet' + sophnet: 'provider.sophnet', + 'ai-gateway': 'provider.ai-gateway', + cerebras: 'provider.cerebras' } as const /** diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 277598c9ef..47ebef1ffb 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "AI Gateway", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Cloud", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "Alibaba Cloud", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "The API version of Azure OpenAI, if you want to use Response API, please enter the preview version" + "tip": "The API version of Azure OpenAI, if you want to use Response API, please enter the v1 version" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 7c5c9b8e3a..aaf337b39e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -904,7 +904,7 @@ "show_line_numbers": "代码显示行号", "temperature": { "label": "模型温度", - "tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7" + "tip": "模型生成文本的随机程度。值越大,回复内容越富有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7" }, "thought_auto_collapse": { "label": "思考内容自动折叠", @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "AI Gateway", "aihubmix": "AiHubMix", "aionly": "唯一AI (AiOnly)", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "百度云千帆", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "阿里云百炼", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Azure OpenAI 的 API 版本,如果想要使用 Response API,请输入 preview 版本" + "tip": "Azure OpenAI 的 API 版本,如果想要使用 Response API,请输入 v1 版本" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 364ccf573e..fc5516b11f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "AI 閘道器", "aihubmix": "AiHubMix", "aionly": "唯一AI (AiOnly)", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "百度雲千帆", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "阿里雲百鍊", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Azure OpenAI 的 API 版本,如果想要使用 Response API,請輸入 preview 版本" + "tip": "Azure OpenAI 的 API 版本,如果想要使用 Response API,請輸入 v1 版本" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 9b51fd3a6a..fbf7f04956 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "KI-Gateway", "aihubmix": "AiHubMix", "aionly": "Einzige KI (AiOnly)", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Cloud Qianfan", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "Alibaba Cloud Bailian", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 00ec9dd9aa..ed87590ce0 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "Πύλη Τεχνητής Νοημοσύνης", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Cloud Qianfan", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "AliCloud Bailian", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Η έκδοση του API για Azure OpenAI. Αν θέλετε να χρησιμοποιήσετε το Response API, εισάγετε μια προεπισκόπηση έκδοσης" + "tip": "Η έκδοση του API για Azure OpenAI. Αν θέλετε να χρησιμοποιήσετε το Response API, εισάγετε μια v1 έκδοσης" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index d17b42cbd6..4cd8f8ad1b 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "Puerta de enlace de IA", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Nube Qiánfān", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copiloto", "dashscope": "Álibaba Nube BaiLiàn", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index f08e127db9..0c62faf907 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "Passerelle IA", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Cloud Qianfan", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilote", "dashscope": "AliCloud BaiLian", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Version de l'API Azure OpenAI, veuillez saisir une version preview si vous souhaitez utiliser l'API de réponse" + "tip": "Version de l'API Azure OpenAI, veuillez saisir une version v1 si vous souhaitez utiliser l'API de réponse" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 3fa82cbd57..d67c26c968 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "AIゲートウェイ", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Cloud", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "Alibaba Cloud", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Azure OpenAIのAPIバージョン。Response APIを使用する場合は、previewバージョンを入力してください" + "tip": "Azure OpenAIのAPIバージョン。Response APIを使用する場合は、v1バージョンを入力してください" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index c3431cac53..968167906b 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "Gateway de IA", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Nuvem Baidu", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copiloto", "dashscope": "Área de Atuação AliCloud", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Versão da API do Azure OpenAI. Se desejar usar a API de Resposta, insira a versão de visualização" + "tip": "Versão da API do Azure OpenAI. Se desejar usar a API de Resposta, insira a versão de v1" } }, "basic_auth": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index e14352886d..5e60e7247c 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -2484,6 +2484,7 @@ }, "provider": { "302ai": "302.AI", + "ai-gateway": "AI-шлюз", "aihubmix": "AiHubMix", "aionly": "AiOnly", "alayanew": "Alaya NeW", @@ -2494,6 +2495,7 @@ "baidu-cloud": "Baidu Cloud", "burncloud": "BurnCloud", "cephalon": "Cephalon", + "cerebras": "Cerebras AI", "cherryin": "CherryIN", "copilot": "GitHub Copilot", "dashscope": "Alibaba Cloud", @@ -4341,7 +4343,7 @@ }, "azure": { "apiversion": { - "tip": "Версия API Azure OpenAI. Если вы хотите использовать Response API, введите версию preview" + "tip": "Версия API Azure OpenAI. Если вы хотите использовать Response API, введите версию v1" } }, "basic_auth": { diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index 742dc8ca22..1cb445f12d 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -205,14 +205,15 @@ const ErrorDetailModal: React.FC = ({ open, onClose, erro {t('common.close')} ]} - width={600}> + width="80%" + style={{ maxWidth: '1200px', minWidth: '600px' }}> {renderErrorDetails(error)} ) } const ErrorDetailContainer = styled.div` - max-height: 400px; + max-height: 60vh; overflow-y: auto; ` @@ -345,16 +346,8 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { return ( - - {(isSerializedAiSdkAPICallError(error) || isSerializedAiSdkDownloadError(error)) && ( <> - {error.statusCode && ( - - {t('error.statusCode')}: - {error.statusCode} - - )} {error.url && ( {t('error.requestUrl')}: @@ -372,12 +365,27 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { )} + + )} - {error.requestBodyValues && ( + {(isSerializedAiSdkAPICallError(error) || isSerializedAiSdkDownloadError(error)) && ( + <> + {error.statusCode && ( - {t('error.requestBodyValues')}: + {t('error.statusCode')}: + {error.statusCode} + + )} + + )} + + {isSerializedAiSdkAPICallError(error) && ( + <> + {error.responseHeaders && ( + + {t('error.responseHeaders')}: { )} - {error.responseHeaders && ( + {error.requestBodyValues && ( - {t('error.responseHeaders')}: + {t('error.requestBodyValues')}: { {error.functionality} )} + + ) } diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 3d0a94faad..5d70f8f3d1 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -11,7 +11,8 @@ import type { Message } from '@renderer/types/newMessage' import { classNames } from '@renderer/utils' import type { MultiModelMessageStyle } from '@shared/data/preference/preferenceTypes' import { Popover } from 'antd' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import type { ComponentProps } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import styled from 'styled-components' import { useChatMaxWidth } from '../Chat' @@ -45,9 +46,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { ) const [selectedIndex, setSelectedIndex] = useState(messageLength - 1) - // Refs - const prevMessageLengthRef = useRef(messageLength) - // 对于单模型消息,采用简单的样式,避免 overflow 影响内部的 sticky 效果 const multiModelMessageStyle = useMemo( () => (messageLength < 2 ? 'fold' : _multiModelMessageStyle), @@ -85,24 +83,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { }, [editMessage, selectedMessageId, setTimeoutTimer] ) - - useEffect(() => { - if (messageLength > prevMessageLengthRef.current) { - setSelectedIndex(messageLength - 1) - const lastMessage = messages[messageLength - 1] - if (lastMessage) { - setSelectedMessage(lastMessage) - } - } else { - const newIndex = messages.findIndex((msg) => msg.id === selectedMessageId) - if (newIndex !== -1) { - setSelectedIndex(newIndex) - } - } - prevMessageLengthRef.current = messageLength - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [messageLength]) - // 添加对流程图节点点击事件的监听 useEffect(() => { // 只在组件挂载和消息数组变化时添加监听器 @@ -225,7 +205,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { message, topic, index: message.index - } + } satisfies ComponentProps const messageContent = ( { isGrouped, topic, multiModelMessageStyle, - messages.length, + messages, selectedMessageId, onUpdateUseful, groupContextMessageId, diff --git a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx b/src/renderer/src/pages/home/Tabs/components/Sessions.tsx index 9373c259d4..af667b942f 100644 --- a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Sessions.tsx @@ -1,4 +1,3 @@ -import Scrollbar from '@renderer/components/Scrollbar' import { DynamicVirtualList } from '@renderer/components/VirtualList' import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession' import { useSessions } from '@renderer/hooks/agents/useSessions' @@ -99,38 +98,36 @@ const Sessions: React.FC = ({ agentId }) => { } return ( - - - {t('agent.session.add.title')} - - {/* h-9 */} - 9 * 4} - scrollerStyle={{ - // FIXME: This component only supports CSSProperties - overflowX: 'hidden' - }} - autoHideScrollbar> - {(session) => ( - handleDeleteSession(session.id)} - onPress={() => setActiveSessionId(agentId, session.id)} - /> - )} - - + 9 * 4} + // FIXME: This component only supports CSSProperties + scrollerStyle={{ overflowX: 'hidden' }} + autoHideScrollbar + header={ + + {t('agent.session.add.title')} + + }> + {(session) => ( + handleDeleteSession(session.id)} + onPress={() => setActiveSessionId(agentId, session.id)} + /> + )} + ) } -const Container = styled(Scrollbar)` +const StyledVirtualList = styled(DynamicVirtualList)` display: flex; flex-direction: column; padding: 12px 10px; - overflow-x: hidden; -` + height: 100%; +` as typeof DynamicVirtualList export default memo(Sessions) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 6593a12519..5aca131594 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -5,6 +5,7 @@ import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup' import Selector from '@renderer/components/Selector' import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' import { + isAIGatewayProvider, isAnthropicProvider, isAzureOpenAIProvider, isGeminiProvider, @@ -80,7 +81,8 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [ SystemProviderIds.aihubmix, SystemProviderIds.grok, SystemProviderIds.cherryin, - SystemProviderIds.longcat + SystemProviderIds.longcat, + SystemProviderIds.minimax ] as const type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number] @@ -305,6 +307,9 @@ const ProviderSetting: FC = ({ providerId }) => { if (isVertexProvider(provider)) { return formatVertexApiHost(provider) + '/publishers/google' } + if (isAIGatewayProvider(provider)) { + return formatApiHost(apiHost) + '/language-model' + } return formatApiHost(apiHost) } @@ -520,24 +525,17 @@ const ProviderSetting: FC = ({ providerId }) => { {t('settings.provider.vertex_ai.api_host_help')} )} - {(isOpenAICompatibleProvider(provider) || - isAzureOpenAIProvider(provider) || - isAnthropicProvider(provider) || - isGeminiProvider(provider) || - isVertexProvider(provider) || - isOpenAIProvider(provider)) && ( - - - {t('settings.provider.api_host_preview', { url: hostPreview() })} - - - )} + + + {t('settings.provider.api_host_preview', { url: hostPreview() })} + + )} @@ -550,6 +548,7 @@ const ProviderSetting: FC = ({ providerId }) => { onChange={(e) => setAnthropicHost(e.target.value)} onBlur={onUpdateAnthropicHost} /> + {/* TODO: Add a reset button here. */} diff --git a/src/renderer/src/services/NotesService.ts b/src/renderer/src/services/NotesService.ts index 4cd73d12dd..940c8db106 100644 --- a/src/renderer/src/services/NotesService.ts +++ b/src/renderer/src/services/NotesService.ts @@ -128,9 +128,9 @@ export async function uploadNotes(files: File[], targetPath: string): Promise number { switch (sortType) { case 'sort_a2z': - return (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'accent' }) + return (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'accent' }) case 'sort_z2a': - return (a, b) => b.name.localeCompare(a.name, undefined, { sensitivity: 'accent' }) + return (a, b) => b.name.localeCompare(a.name, undefined, { numeric: true, sensitivity: 'accent' }) case 'sort_updated_desc': return (a, b) => getTime(b.updatedAt) - getTime(a.updatedAt) case 'sort_updated_asc': @@ -140,7 +140,7 @@ function getSorter(sortType: NotesSortType): (a: NotesTreeNode, b: NotesTreeNode case 'sort_created_asc': return (a, b) => getTime(a.createdAt) - getTime(b.createdAt) default: - return (a, b) => a.name.localeCompare(b.name) + return (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'accent' }) } } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 7e4808f639..1bdcc3b31a 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -71,7 +71,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 173, + version: 174, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 2bef828cc5..0dc99b3213 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2807,6 +2807,23 @@ const migrateConfig = { logger.error('migrate 173 error', error as Error) return state } + }, + '174': (state: RootState) => { + try { + addProvider(state, SystemProviderIds.longcat) + + addProvider(state, SystemProviderIds['ai-gateway']) + addProvider(state, 'cerebras') + state.llm.providers.forEach((provider) => { + if (provider.id === SystemProviderIds.minimax) { + provider.anthropicApiHost = 'https://api.minimaxi.com/anthropic' + } + }) + return state + } catch (error) { + logger.error('migrate 174 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index a377276c2a..96153accd8 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -1447,7 +1447,7 @@ export const appendAssistantResponseThunk = } // 2. Create the new assistant message stub - const newAssistantStub = createAssistantMessage(assistant.id, topicId, { + const newAssistantMessageStub = createAssistantMessage(assistant.id, topicId, { askId: askId, // Crucial: Use the original askId model: newModel, modelId: newModel.id, @@ -1460,9 +1460,14 @@ export const appendAssistantResponseThunk = const insertAtIndex = existingMessageIndex !== -1 ? existingMessageIndex + 1 : currentTopicMessageIds.length // 4. Update Database (Save the stub to the topic's message list) - await saveMessageAndBlocksToDB(newAssistantStub, [], insertAtIndex) + await saveMessageAndBlocksToDB(newAssistantMessageStub, [], insertAtIndex) - dispatch(newMessagesActions.insertMessageAtIndex({ topicId, message: newAssistantStub, index: insertAtIndex })) + dispatch( + newMessagesActions.insertMessageAtIndex({ topicId, message: newAssistantMessageStub, index: insertAtIndex }) + ) + + dispatch(updateMessageAndBlocksThunk(topicId, { id: existingAssistantMessageId, foldSelected: false }, [])) + dispatch(updateMessageAndBlocksThunk(topicId, { id: newAssistantMessageStub.id, foldSelected: true }, [])) // 5. Prepare and queue the processing task const assistantConfigForThisCall = { @@ -1476,7 +1481,7 @@ export const appendAssistantResponseThunk = getState, topicId, assistantConfigForThisCall, - newAssistantStub // Pass the newly created stub + newAssistantMessageStub // Pass the newly created stub ) }) } catch (error) { diff --git a/src/renderer/src/types/provider.ts b/src/renderer/src/types/provider.ts index b7d669e1f2..5bd605007e 100644 --- a/src/renderer/src/types/provider.ts +++ b/src/renderer/src/types/provider.ts @@ -11,7 +11,8 @@ export const ProviderTypeSchema = z.enum([ 'mistral', 'aws-bedrock', 'vertex-anthropic', - 'new-api' + 'new-api', + 'ai-gateway' ]) export type ProviderType = z.infer @@ -176,7 +177,9 @@ export const SystemProviderIds = { poe: 'poe', aionly: 'aionly', longcat: 'longcat', - huggingface: 'huggingface' + huggingface: 'huggingface', + 'ai-gateway': 'ai-gateway', + cerebras: 'cerebras' } as const export type SystemProviderId = keyof typeof SystemProviderIds diff --git a/src/renderer/src/types/sdk.ts b/src/renderer/src/types/sdk.ts index 90a0101563..66e6b3627a 100644 --- a/src/renderer/src/types/sdk.ts +++ b/src/renderer/src/types/sdk.ts @@ -97,6 +97,7 @@ export type ReasoningEffortOptionalParams = { } } } + disable_reasoning?: boolean // Add any other potential reasoning-related keys here if they exist } diff --git a/src/renderer/src/utils/dom.ts b/src/renderer/src/utils/dom.ts new file mode 100644 index 0000000000..6dd09cda5e --- /dev/null +++ b/src/renderer/src/utils/dom.ts @@ -0,0 +1,55 @@ +/** + * Simple wrapper for scrollIntoView with common default options. + * Provides a unified interface with sensible defaults. + * + * @param element - The target element to scroll into view + * @param options - Scroll options. If not provided, uses { behavior: 'smooth', block: 'center', inline: 'nearest' } + */ +export function scrollIntoView(element: HTMLElement, options?: ScrollIntoViewOptions): void { + const defaultOptions: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + inline: 'nearest' + } + element.scrollIntoView(options ?? defaultOptions) +} + +/** + * Intelligently scrolls an element into view at the center position. + * Prioritizes scrolling within the specified container to avoid scrolling the entire page. + * + * @param element - The target element to scroll into view + * @param scrollContainer - Optional scroll container. If provided and scrollable, scrolling happens within it; otherwise uses browser default scrolling + * @param behavior - Scroll behavior, defaults to 'smooth' + */ +export function scrollElementIntoView( + element: HTMLElement, + scrollContainer?: HTMLElement | null, + behavior: ScrollBehavior = 'smooth' +): void { + if (!scrollContainer) { + // No container specified, use browser default scrolling + scrollIntoView(element, { behavior, block: 'center', inline: 'nearest' }) + return + } + + // Check if container is scrollable + const canScroll = + scrollContainer.scrollHeight > scrollContainer.clientHeight || + scrollContainer.scrollWidth > scrollContainer.clientWidth + + if (canScroll) { + // Container is scrollable, scroll within the container + const containerRect = scrollContainer.getBoundingClientRect() + const elRect = element.getBoundingClientRect() + + // Calculate element's scrollable offset position relative to the container + const elementTopWithinContainer = elRect.top - containerRect.top + scrollContainer.scrollTop + const desiredTop = elementTopWithinContainer - Math.max(0, scrollContainer.clientHeight - elRect.height) / 2 + + scrollContainer.scrollTo({ top: Math.max(0, desiredTop), behavior }) + } else { + // Container is not scrollable, fallback to browser default scrolling + scrollIntoView(element, { behavior, block: 'center', inline: 'nearest' }) + } +} diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index 8f1b260fcc..d8b31c7564 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -220,6 +220,7 @@ export function uniqueObjectArray(array: T[]): T[] { export * from './api' export * from './collection' export * from './dataLimit' +export * from './dom' export * from './file' export * from './image' export * from './json' diff --git a/yarn.lock b/yarn.lock index 6417b51312..a29a34d9b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,6 +127,19 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/cerebras@npm:^1.0.31": + version: 1.0.31 + resolution: "@ai-sdk/cerebras@npm:1.0.31" + dependencies: + "@ai-sdk/openai-compatible": "npm:1.0.27" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.17" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/0723f0041b767acfb7a9d903d51d5c95af83c31c89b83f242cb5c02a076d8b98f6567334eb32dcdbc8565b55ded2aa5195ca68612bbe7b13e68253cf4ef412d6 + languageName: node + linkType: hard + "@ai-sdk/deepseek@npm:^1.0.27": version: 1.0.27 resolution: "@ai-sdk/deepseek@npm:1.0.27" @@ -153,6 +166,19 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/gateway@npm:^2.0.9": + version: 2.0.9 + resolution: "@ai-sdk/gateway@npm:2.0.9" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.17" + "@vercel/oidc": "npm:3.0.3" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/840f94795b96c0fa6e73897ea8dba95fc78af1f8482f3b7d8439b6233b4f4de6979a8b67206f4bbf32649baf2acfb1153a46792dfa20259ca9f5fd214fb25fa5 + languageName: node + linkType: hard + "@ai-sdk/google-vertex@npm:^3.0.62": version: 3.0.62 resolution: "@ai-sdk/google-vertex@npm:3.0.62" @@ -242,6 +268,18 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/openai-compatible@npm:1.0.27": + version: 1.0.27 + resolution: "@ai-sdk/openai-compatible@npm:1.0.27" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.17" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/9f656e4f2ea4d714dc05be588baafd962b2e0360e9195fef373e745efeb20172698ea87e1033c0c5e1f1aa6e0db76a32629427bc8433eb42bd1a0ee00e04af0c + languageName: node + linkType: hard + "@ai-sdk/openai-compatible@npm:^1.0.19": version: 1.0.19 resolution: "@ai-sdk/openai-compatible@npm:1.0.19" @@ -290,7 +328,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:3.0.10, @ai-sdk/provider-utils@npm:^3.0.10": +"@ai-sdk/provider-utils@npm:3.0.10": version: 3.0.10 resolution: "@ai-sdk/provider-utils@npm:3.0.10" dependencies: @@ -316,7 +354,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:3.0.17, @ai-sdk/provider-utils@npm:^3.0.12": +"@ai-sdk/provider-utils@npm:3.0.17, @ai-sdk/provider-utils@npm:^3.0.10": version: 3.0.17 resolution: "@ai-sdk/provider-utils@npm:3.0.17" dependencies: @@ -329,6 +367,19 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/provider-utils@npm:^3.0.12": + version: 3.0.12 + resolution: "@ai-sdk/provider-utils@npm:3.0.12" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.5" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/83886bf188cad0cc655b680b710a10413989eaba9ec59dd24a58b985c02a8a1d50ad0f96dd5259385c07592ec3c37a7769fdf4a1ef569a73c9edbdb2cd585915 + languageName: node + linkType: hard + "@ai-sdk/provider@npm:2.0.0, @ai-sdk/provider@npm:^2.0.0": version: 2.0.0 resolution: "@ai-sdk/provider@npm:2.0.0" @@ -2868,26 +2919,6 @@ __metadata: languageName: node linkType: hard -"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2": - version: 10.2.0-electron.1 - resolution: "@electron/node-gyp@https://github.com/electron/node-gyp.git#commit=06b29aafb7708acef8b3669835c8a7857ebc92d2" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - glob: "npm:^8.1.0" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^10.2.1" - nopt: "npm:^6.0.0" - proc-log: "npm:^2.0.1" - semver: "npm:^7.3.5" - tar: "npm:^6.2.1" - which: "npm:^2.0.2" - bin: - node-gyp: ./bin/node-gyp.js - checksum: 10c0/e8c97bb5347bf0871312860010b70379069359bf05a6beb9e4d898d0831f9f8447f35b887a86d5241989e804813cf72054327928da38714a6102f791e802c8d9 - languageName: node - linkType: hard - "@electron/notarize@npm:2.5.0, @electron/notarize@npm:^2.5.0": version: 2.5.0 resolution: "@electron/notarize@npm:2.5.0" @@ -2916,20 +2947,19 @@ __metadata: languageName: node linkType: hard -"@electron/rebuild@npm:3.7.2": - version: 3.7.2 - resolution: "@electron/rebuild@npm:3.7.2" +"@electron/rebuild@npm:4.0.1": + version: 4.0.1 + resolution: "@electron/rebuild@npm:4.0.1" dependencies: - "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2" "@malept/cross-spawn-promise": "npm:^2.0.0" chalk: "npm:^4.0.0" debug: "npm:^4.1.1" detect-libc: "npm:^2.0.1" - fs-extra: "npm:^10.0.0" got: "npm:^11.7.0" - node-abi: "npm:^3.45.0" - node-api-version: "npm:^0.2.0" - node-gyp: "npm:latest" + graceful-fs: "npm:^4.2.11" + node-abi: "npm:^4.2.0" + node-api-version: "npm:^0.2.1" + node-gyp: "npm:^11.2.0" ora: "npm:^5.1.0" read-binary-file-arch: "npm:^1.0.6" semver: "npm:^7.3.5" @@ -2937,7 +2967,7 @@ __metadata: yargs: "npm:^17.0.1" bin: electron-rebuild: lib/cli.js - checksum: 10c0/e561819926c30c7ad284f721d1d66453f59f8e5ea54a7cc9148a00e8ab3cedb6aa57fe4789f39a3454a3eb90b41a5f7d7461246ee3a16c63c8d3db23db94a391 + checksum: 10c0/4863d39c34515f3fb521ce5e976e25db20e89920574ca353efd0c3272d5f4d14546ba15ce28cee4299187160b2af02e3e130100ba8dc53f313b6eb685dc54928 languageName: node linkType: hard @@ -3532,13 +3562,6 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 10c0/0b3c9958d3cd17f4add3574975e3115ae05dc7f1298a60810414b16f6f558c137b5fb3cd3905df380bacfd955ec13f67c1e6710cbb5c246a7e8d65a8289b2bff - languageName: node - linkType: hard - "@google/genai@npm:1.0.1": version: 1.0.1 resolution: "@google/genai@npm:1.0.1" @@ -5325,6 +5348,22 @@ __metadata: languageName: node linkType: hard +"@isaacs/balanced-match@npm:^4.0.1": + version: 4.0.1 + resolution: "@isaacs/balanced-match@npm:4.0.1" + checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420 + languageName: node + linkType: hard + +"@isaacs/brace-expansion@npm:^5.0.0": + version: 5.0.0 + resolution: "@isaacs/brace-expansion@npm:5.0.0" + dependencies: + "@isaacs/balanced-match": "npm:^4.0.1" + checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -6532,16 +6571,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.2 - resolution: "@npmcli/fs@npm:2.1.2" - dependencies: - "@gar/promisify": "npm:^1.1.3" - semver: "npm:^7.3.5" - checksum: 10c0/c50d087733d0d8df23be24f700f104b19922a28677aa66fdbe06ff6af6431cc4a5bb1e27683cbc661a5dafa9bafdc603e6a0378121506dfcd394b2b6dd76a187 - languageName: node - linkType: hard - "@npmcli/fs@npm:^4.0.0": version: 4.0.0 resolution: "@npmcli/fs@npm:4.0.0" @@ -6551,16 +6580,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.1 - resolution: "@npmcli/move-file@npm:2.0.1" - dependencies: - mkdirp: "npm:^1.0.4" - rimraf: "npm:^3.0.2" - checksum: 10c0/11b2151e6d1de6f6eb23128de5aa8a429fd9097d839a5190cb77aa47a6b627022c42d50fa7c47a00f1c9f8f0c1560092b09b061855d293fa0741a2a94cfb174d - languageName: node - linkType: hard - "@openrouter/ai-sdk-provider@npm:^1.2.0": version: 1.2.0 resolution: "@openrouter/ai-sdk-provider@npm:1.2.0" @@ -11458,13 +11477,6 @@ __metadata: languageName: node linkType: hard -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: 10c0/073bfa548026b1ebaf1659eb8961e526be22fa77139b10d60e712f46d2f0f05f4e6c8bec62a087d41088ee9e29faa7f54838568e475ab2f776171003c3920858 - languageName: node - linkType: hard - "@tootallnate/quickjs-emscripten@npm:^0.23.0": version: 0.23.0 resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" @@ -13493,6 +13505,8 @@ __metadata: "@agentic/searxng": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3" "@ai-sdk/amazon-bedrock": "npm:^3.0.53" + "@ai-sdk/cerebras": "npm:^1.0.31" + "@ai-sdk/gateway": "npm:^2.0.9" "@ai-sdk/google-vertex": "npm:^3.0.62" "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch" "@ai-sdk/mistral": "npm:^2.0.23" @@ -13649,12 +13663,12 @@ __metadata: dotenv-cli: "npm:^7.4.2" drizzle-kit: "npm:^0.31.4" drizzle-orm: "npm:^0.44.5" - electron: "npm:38.4.0" - electron-builder: "npm:26.0.15" + electron: "npm:38.7.0" + electron-builder: "npm:26.1.0" electron-devtools-installer: "npm:^3.2.0" electron-reload: "npm:^2.0.0-alpha.1" electron-store: "npm:^8.2.0" - electron-updater: "npm:6.6.4" + electron-updater: "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch" electron-vite: "npm:4.0.1" electron-window-state: "npm:^5.0.3" emittery: "npm:^1.0.3" @@ -13784,13 +13798,6 @@ __metadata: languageName: unknown linkType: soft -"abbrev@npm:^1.0.0": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 10c0/3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 - languageName: node - linkType: hard - "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -13852,15 +13859,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: "npm:4" - checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 - languageName: node - linkType: hard - "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": version: 7.1.3 resolution: "agent-base@npm:7.1.3" @@ -13877,16 +13875,6 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: "npm:^2.0.0" - indent-string: "npm:^4.0.0" - checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 - languageName: node - linkType: hard - "ai@npm:^5.0.90": version: 5.0.90 resolution: "ai@npm:5.0.90" @@ -14205,91 +14193,48 @@ __metadata: languageName: node linkType: hard -"app-builder-lib@npm:26.0.15": - version: 26.0.15 - resolution: "app-builder-lib@npm:26.0.15" +"app-builder-lib@npm:26.1.0": + version: 26.1.0 + resolution: "app-builder-lib@npm:26.1.0" dependencies: "@develar/schema-utils": "npm:~2.6.5" "@electron/asar": "npm:3.4.1" "@electron/fuses": "npm:^1.8.0" "@electron/notarize": "npm:2.5.0" "@electron/osx-sign": "npm:1.3.3" - "@electron/rebuild": "npm:3.7.2" + "@electron/rebuild": "npm:4.0.1" "@electron/universal": "npm:2.0.3" "@malept/flatpak-bundler": "npm:^0.4.0" "@types/fs-extra": "npm:9.0.13" async-exit-hook: "npm:^2.0.1" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + builder-util: "npm:26.1.0" + builder-util-runtime: "npm:9.5.0" chromium-pickle-js: "npm:^0.2.0" - config-file-ts: "npm:0.2.8-rc1" + ci-info: "npm:^4.2.0" debug: "npm:^4.3.4" dotenv: "npm:^16.4.5" dotenv-expand: "npm:^11.0.6" ejs: "npm:^3.1.8" - electron-publish: "npm:26.0.13" + electron-publish: "npm:26.1.0" fs-extra: "npm:^10.1.0" hosted-git-info: "npm:^4.1.0" - is-ci: "npm:^3.0.0" isbinaryfile: "npm:^5.0.0" + jiti: "npm:^2.4.2" js-yaml: "npm:^4.1.0" json5: "npm:^2.2.3" lazy-val: "npm:^1.0.5" - minimatch: "npm:^10.0.0" + minimatch: "npm:^10.0.3" plist: "npm:3.1.0" resedit: "npm:^1.7.0" - semver: "npm:^7.3.8" + semver: "npm:7.7.2" tar: "npm:^6.1.12" temp-file: "npm:^3.4.0" tiny-async-pool: "npm:1.3.0" + which: "npm:^5.0.0" peerDependencies: - dmg-builder: 26.0.15 - electron-builder-squirrel-windows: 26.0.15 - checksum: 10c0/d617864aca3c61633a9a5fda8d991ea90bcbe502702a4a1d64545ae6cfaa1f4122415db02b11e0dc76a527b169ea6e5619551903456e24a7053b3f4eb04cb79f - languageName: node - linkType: hard - -"app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch": - version: 26.0.15 - resolution: "app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch::version=26.0.15&hash=1f4887" - dependencies: - "@develar/schema-utils": "npm:~2.6.5" - "@electron/asar": "npm:3.4.1" - "@electron/fuses": "npm:^1.8.0" - "@electron/notarize": "npm:2.5.0" - "@electron/osx-sign": "npm:1.3.3" - "@electron/rebuild": "npm:3.7.2" - "@electron/universal": "npm:2.0.3" - "@malept/flatpak-bundler": "npm:^0.4.0" - "@types/fs-extra": "npm:9.0.13" - async-exit-hook: "npm:^2.0.1" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" - chromium-pickle-js: "npm:^0.2.0" - config-file-ts: "npm:0.2.8-rc1" - debug: "npm:^4.3.4" - dotenv: "npm:^16.4.5" - dotenv-expand: "npm:^11.0.6" - ejs: "npm:^3.1.8" - electron-publish: "npm:26.0.13" - fs-extra: "npm:^10.1.0" - hosted-git-info: "npm:^4.1.0" - is-ci: "npm:^3.0.0" - isbinaryfile: "npm:^5.0.0" - js-yaml: "npm:^4.1.0" - json5: "npm:^2.2.3" - lazy-val: "npm:^1.0.5" - minimatch: "npm:^10.0.0" - plist: "npm:3.1.0" - resedit: "npm:^1.7.0" - semver: "npm:^7.3.8" - tar: "npm:^6.1.12" - temp-file: "npm:^3.4.0" - tiny-async-pool: "npm:1.3.0" - peerDependencies: - dmg-builder: 26.0.15 - electron-builder-squirrel-windows: 26.0.15 - checksum: 10c0/5de2bd593b21e464585ffa3424e053d41f8569b14ba2a00f29f84cb0b83347a7da3653587f9ef8b5d2f6d1e5bfc4081956b9d72f180d65960db49b5ac84b73d4 + dmg-builder: 26.1.0 + electron-builder-squirrel-windows: 26.1.0 + checksum: 10c0/c8397886e59dc6a8ae4d90bc59fd28631705c5873789463a55b3e029062d6194d38e9feb1e6595ca31a069ed37ae893703fadd09a95ed4d2b1ab92fb92b13d72 languageName: node linkType: hard @@ -14846,38 +14791,38 @@ __metadata: languageName: node linkType: hard -"builder-util-runtime@npm:9.3.2": - version: 9.3.2 - resolution: "builder-util-runtime@npm:9.3.2" +"builder-util-runtime@npm:9.5.0": + version: 9.5.0 + resolution: "builder-util-runtime@npm:9.5.0" dependencies: debug: "npm:^4.3.4" sax: "npm:^1.2.4" - checksum: 10c0/1a103268ef800a504f04021ce14db282ddcfb72dec8238e7c9624a9c651ccd9c15c45ddcdb00e7cf6a5164d9822e30efdeeff470b506ed6aa9ed27c0aaefa695 + checksum: 10c0/797f4f8129557de6f5699991974f1701e464646664a14f841870fca0ddb05cb63cb8f2ca3c082cd6215690048c5e12df8404e7ccec371640eed9edc8cb592ae6 languageName: node linkType: hard -"builder-util@npm:26.0.13": - version: 26.0.13 - resolution: "builder-util@npm:26.0.13" +"builder-util@npm:26.1.0": + version: 26.1.0 + resolution: "builder-util@npm:26.1.0" dependencies: 7zip-bin: "npm:~5.2.0" "@types/debug": "npm:^4.1.6" app-builder-bin: "npm:5.0.0-alpha.12" - builder-util-runtime: "npm:9.3.2" + builder-util-runtime: "npm:9.5.0" chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.4" fs-extra: "npm:^10.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.0" - is-ci: "npm:^3.0.0" js-yaml: "npm:^4.1.0" sanitize-filename: "npm:^1.6.3" source-map-support: "npm:^0.5.19" stat-mode: "npm:^1.0.0" temp-file: "npm:^3.4.0" tiny-async-pool: "npm:1.3.0" - checksum: 10c0/e8e9d6de04ec5c60f21c8ac8a30f6edd38ae76f0438ba801ec135ddecdce7c4bfadf881bdaaed184b0cab28e04ef21869ecc67a1e44c0e38ec0fd56c90970f03 + checksum: 10c0/0e1bcc04452cda8eaa1d63f338e05c1280f0539ee9dd7a9d4d17f75dff323d0d34de184fc146e3bdb1e1f1578bc0070569b1701312b509e802c97bfe4fed24b1 languageName: node linkType: hard @@ -14902,32 +14847,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.3 - resolution: "cacache@npm:16.1.3" - dependencies: - "@npmcli/fs": "npm:^2.1.0" - "@npmcli/move-file": "npm:^2.0.0" - chownr: "npm:^2.0.0" - fs-minipass: "npm:^2.1.0" - glob: "npm:^8.0.1" - infer-owner: "npm:^1.0.4" - lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - mkdirp: "npm:^1.0.4" - p-map: "npm:^4.0.0" - promise-inflight: "npm:^1.0.1" - rimraf: "npm:^3.0.2" - ssri: "npm:^9.0.0" - tar: "npm:^6.1.11" - unique-filename: "npm:^2.0.0" - checksum: 10c0/cdf6836e1c457d2a5616abcaf5d8240c0346b1f5bd6fdb8866b9d84b6dff0b54e973226dc11e0d099f35394213d24860d1989c8358d2a41b39eb912b3000e749 - languageName: node - linkType: hard - "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -15320,10 +15239,10 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0": - version: 3.9.0 - resolution: "ci-info@npm:3.9.0" - checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a +"ci-info@npm:^4.2.0": + version: 4.3.1 + resolution: "ci-info@npm:4.3.1" + checksum: 10c0/7dd82000f514d76ddfe7775e4cb0d66e5c638f5fa0e2a3be29557e898da0d32ac04f231217d414d07fb968b1fbc6d980ee17ddde0d2c516f23da9cfff608f6c1 languageName: node linkType: hard @@ -15357,13 +15276,6 @@ __metadata: languageName: node linkType: hard -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 - languageName: node - linkType: hard - "cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -15831,16 +15743,6 @@ __metadata: languageName: node linkType: hard -"config-file-ts@npm:0.2.8-rc1": - version: 0.2.8-rc1 - resolution: "config-file-ts@npm:0.2.8-rc1" - dependencies: - glob: "npm:^10.3.12" - typescript: "npm:^5.4.3" - checksum: 10c0/9839a8e33111156665c45c4e5dd6bfa81ee80596f9dc0a078465769b951e28c0fa4bd75bb9bc56f747da285b993fb7998c4c07c0f368ab6bdb019d203764cdc8 - languageName: node - linkType: hard - "console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -16605,7 +16507,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -16977,13 +16879,12 @@ __metadata: languageName: node linkType: hard -"dmg-builder@npm:26.0.15": - version: 26.0.15 - resolution: "dmg-builder@npm:26.0.15" +"dmg-builder@npm:26.1.0": + version: 26.1.0 + resolution: "dmg-builder@npm:26.1.0" dependencies: - app-builder-lib: "npm:26.0.15" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + app-builder-lib: "npm:26.1.0" + builder-util: "npm:26.1.0" dmg-license: "npm:^1.0.11" fs-extra: "npm:^10.1.0" iconv-lite: "npm:^0.6.2" @@ -16991,7 +16892,7 @@ __metadata: dependenciesMeta: dmg-license: optional: true - checksum: 10c0/fe9ea305abf05e16d96f7f7435db14f0264f82d4f49a09a64645425a3d2a69ed9cc346f36278b958fb1197fea72f5afc9661a22307d2c0ab5192843dc31f794c + checksum: 10c0/0dc4e993516dfb896b45b7de6ee88bc99a95205e64bbcac4425dba4fc3b608d5117f8ff14c4204ae916cb567b7c1ab5acc91fa223856ed66e9f22446d440c3dc languageName: node linkType: hard @@ -17341,24 +17242,24 @@ __metadata: languageName: node linkType: hard -"electron-builder@npm:26.0.15": - version: 26.0.15 - resolution: "electron-builder@npm:26.0.15" +"electron-builder@npm:26.1.0": + version: 26.1.0 + resolution: "electron-builder@npm:26.1.0" dependencies: - app-builder-lib: "npm:26.0.15" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + app-builder-lib: "npm:26.1.0" + builder-util: "npm:26.1.0" + builder-util-runtime: "npm:9.5.0" chalk: "npm:^4.1.2" - dmg-builder: "npm:26.0.15" + ci-info: "npm:^4.2.0" + dmg-builder: "npm:26.1.0" fs-extra: "npm:^10.1.0" - is-ci: "npm:^3.0.0" lazy-val: "npm:^1.0.5" simple-update-notifier: "npm:2.0.0" yargs: "npm:^17.6.2" bin: electron-builder: cli.js install-app-deps: install-app-deps.js - checksum: 10c0/bb21e4b547c8dfa590017930340ab9b7e2b017c5ba9286e5d0ccbe6481f4b13bbf905429124a1350a2282ee35dd52e9ba9d9d1d730fc1957c9e7789d0eb39374 + checksum: 10c0/9255a77f1124d3bc722ce9670380144eda42508f8a4695cad5346a44a7b547febe09e736b1b0046b7ddf84c4ea07ab385f87e2c8053dfa996a823d79e2bd05c8 languageName: node linkType: hard @@ -17374,19 +17275,19 @@ __metadata: languageName: node linkType: hard -"electron-publish@npm:26.0.13": - version: 26.0.13 - resolution: "electron-publish@npm:26.0.13" +"electron-publish@npm:26.1.0": + version: 26.1.0 + resolution: "electron-publish@npm:26.1.0" dependencies: "@types/fs-extra": "npm:^9.0.11" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + builder-util: "npm:26.1.0" + builder-util-runtime: "npm:9.5.0" chalk: "npm:^4.1.2" form-data: "npm:^4.0.0" fs-extra: "npm:^10.1.0" lazy-val: "npm:^1.0.5" mime: "npm:^2.5.2" - checksum: 10c0/d00fd7bb904a9cf7731f194eef47147febc9c2b23b1003a00e8d678c04d00029f998cdccd9a9cacacbb46893741961137e92d392e1bb946019c4fc51ceedc922 + checksum: 10c0/f6593e007f47bea311ab9678c31f724a3c0826de4e0f8ea917d4c3d073d3470ede6a093b51408cd53dd790bb1baa4d5b7647a8cd935d0ff3b4d011050e861f0b languageName: node linkType: hard @@ -17416,19 +17317,35 @@ __metadata: languageName: node linkType: hard -"electron-updater@npm:6.6.4": - version: 6.6.4 - resolution: "electron-updater@npm:6.6.4" +"electron-updater@npm:6.7.0": + version: 6.7.0 + resolution: "electron-updater@npm:6.7.0" dependencies: - builder-util-runtime: "npm:9.3.2" + builder-util-runtime: "npm:9.5.0" fs-extra: "npm:^10.1.0" js-yaml: "npm:^4.1.0" lazy-val: "npm:^1.0.5" lodash.escaperegexp: "npm:^4.1.2" lodash.isequal: "npm:^4.5.0" - semver: "npm:^7.6.3" + semver: "npm:7.7.2" tiny-typed-emitter: "npm:^2.1.0" - checksum: 10c0/92ed7b39be1cf9cfe7be56e1054c44a405421ef7faeec365f8c19a6614428ec1e5116ba1f0fb731691d346e00cf16a555804e41feee2b9ac1a10759219de4406 + checksum: 10c0/8310af4a0a795de4bc68a75dc87e4116be1cfc324b60b60492b4afa2a8866ef58a53aa888d8709bed3fba4202deac8a9f02bf7ba03cb1a8fdbbed1a6fb1dad31 + languageName: node + linkType: hard + +"electron-updater@patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch": + version: 6.7.0 + resolution: "electron-updater@patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch::version=6.7.0&hash=5680de" + dependencies: + builder-util-runtime: "npm:9.5.0" + fs-extra: "npm:^10.1.0" + js-yaml: "npm:^4.1.0" + lazy-val: "npm:^1.0.5" + lodash.escaperegexp: "npm:^4.1.2" + lodash.isequal: "npm:^4.5.0" + semver: "npm:7.7.2" + tiny-typed-emitter: "npm:^2.1.0" + checksum: 10c0/8f80f2d76a254404abc43d9c03b68cf5a0d8ff933aa2d43d77f13d24f58e28a903828dac244c05b3391497d53e94d36452066920feb3c1b04ebdcf91faf47293 languageName: node linkType: hard @@ -17464,16 +17381,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:38.4.0": - version: 38.4.0 - resolution: "electron@npm:38.4.0" +"electron@npm:38.7.0": + version: 38.7.0 + resolution: "electron@npm:38.7.0" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^22.7.7" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10c0/3458409151d12f1fcd5e95374aa36e0d2f4aa0d3421c9f57dc521c606070294f33b24a681b3f93b49b02f4a3a07eb0070100ebda51b1198efd4b49dbf1260713 + checksum: 10c0/78a0917141b7a90253aff16e83b9683fb0facb098e8d9d5a71e7100b15fc3c00cd5d92e2ed3aba70067365022920293a7335ccfda5e8de1ef0d9a7d350e24c3c languageName: node linkType: hard @@ -19010,7 +18927,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": +"fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -19268,7 +19185,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1": +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.7, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -19298,19 +19215,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1, glob@npm:^8.1.0": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^5.0.1" - once: "npm:^1.3.0" - checksum: 10c0/cb0b5cab17a59c57299376abe5646c7070f8acb89df5595b492dba3bfb43d301a46c01e5695f01154e6553168207cb60d4eaf07d3be4bc3eb9b0457c5c561d0f - languageName: node - linkType: hard - "global-agent@npm:^3.0.0": version: 3.0.0 resolution: "global-agent@npm:3.0.0" @@ -19880,7 +19784,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": +"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 @@ -19900,17 +19804,6 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" - dependencies: - "@tootallnate/once": "npm:2" - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/32a05e413430b2c1e542e5c74b38a9f14865301dd69dff2e53ddb684989440e3d2ce0c4b64d25eb63cf6283e6265ff979a61cf93e3ca3d23047ddfdc8df34a32 - languageName: node - linkType: hard - "http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" @@ -19941,16 +19834,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 - languageName: node - linkType: hard - "https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -20082,13 +19965,6 @@ __metadata: languageName: node linkType: hard -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 10c0/a7b241e3149c26e37474e3435779487f42f36883711f198c45794703c7556bc38af224088bd4d1a221a45b8208ae2c2bcf86200383621434d0c099304481c5b9 - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -20251,17 +20127,6 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^3.0.0": - version: 3.0.1 - resolution: "is-ci@npm:3.0.1" - dependencies: - ci-info: "npm:^3.2.0" - bin: - is-ci: bin.js - checksum: 10c0/0e81caa62f4520d4088a5bef6d6337d773828a88610346c4b1119fb50c842587ed8bef1e5d9a656835a599e7209405b5761ddf2339668f2d0f4e889a92fe6051 - languageName: node - linkType: hard - "is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" @@ -20391,13 +20256,6 @@ __metadata: languageName: node linkType: hard -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d - languageName: node - linkType: hard - "is-natural-number@npm:^4.0.1": version: 4.0.1 resolution: "is-natural-number@npm:4.0.1" @@ -20626,6 +20484,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^2.4.2": + version: 2.6.1 + resolution: "jiti@npm:2.6.1" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10c0/79b2e96a8e623f66c1b703b98ec1b8be4500e1d217e09b09e343471bbb9c105381b83edbb979d01cef18318cc45ce6e153571b6c83122170eefa531c64b6789b + languageName: node + linkType: hard + "jiti@npm:^2.5.1": version: 2.5.1 resolution: "jiti@npm:2.5.1" @@ -21676,7 +21543,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.14.1": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" checksum: 10c0/b3a452b491433db885beed95041eb104c157ef7794b9c9b4d647be503be91769d11206bb573849a16b4cc0d03cbd15ffd22df7960997788b74c1d399ac7a4fed @@ -21762,30 +21629,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.2.1": - version: 10.2.1 - resolution: "make-fetch-happen@npm:10.2.1" - dependencies: - agentkeepalive: "npm:^4.2.1" - cacache: "npm:^16.1.0" - http-cache-semantics: "npm:^4.1.0" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.0" - is-lambda: "npm:^1.0.1" - lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-fetch: "npm:^2.0.3" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^0.6.3" - promise-retry: "npm:^2.0.1" - socks-proxy-agent: "npm:^7.0.0" - ssri: "npm:^9.0.0" - checksum: 10c0/28ec392f63ab93511f400839dcee83107eeecfaad737d1e8487ea08b4332cd89a8f3319584222edd9f6f1d0833cf516691469496d46491863f9e88c658013949 - languageName: node - linkType: hard - "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -22973,12 +22816,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.0": - version: 10.0.1 - resolution: "minimatch@npm:10.0.1" +"minimatch@npm:^10.0.3": + version: 10.1.1 + resolution: "minimatch@npm:10.1.1" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10c0/e6c29a81fe83e1877ad51348306be2e8aeca18c88fdee7a99df44322314279e15799e41d7cb274e4e8bb0b451a3bc622d6182e157dfa1717d6cda75e9cd8cd5d + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10c0/c85d44821c71973d636091fddbfbffe62370f5ee3caf0241c5b60c18cd289e916200acb2361b7e987558cd06896d153e25d505db9fc1e43e6b4b6752e2702902 languageName: node linkType: hard @@ -23016,15 +22859,6 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/8f82bd1f3095b24f53a991b04b67f4c710c894e518b813f0864a31de5570441a509be1ca17e0bb92b047591a8fdbeb886f502764fefb00d2f144f4011791e898 - languageName: node - linkType: hard - "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -23034,21 +22868,6 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^2.0.3": - version: 2.1.2 - resolution: "minipass-fetch@npm:2.1.2" - dependencies: - encoding: "npm:^0.1.13" - minipass: "npm:^3.1.6" - minipass-sized: "npm:^1.0.3" - minizlib: "npm:^2.1.2" - dependenciesMeta: - encoding: - optional: true - checksum: 10c0/33ab2c5bdb3d91b9cb8bc6ae42d7418f4f00f7f7beae14b3bb21ea18f9224e792f560a6e17b6f1be12bbeb70dbe99a269f4204c60e5d99130a0777b153505c43 - languageName: node - linkType: hard - "minipass-fetch@npm:^4.0.0": version: 4.0.1 resolution: "minipass-fetch@npm:4.0.1" @@ -23091,7 +22910,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": +"minipass@npm:^3.0.0": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -23114,7 +22933,7 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": +"minizlib@npm:^2.1.1": version: 2.1.2 resolution: "minizlib@npm:2.1.2" dependencies: @@ -23158,7 +22977,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": +"mkdirp@npm:^1.0.3": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" bin: @@ -23329,13 +23148,6 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": - version: 0.6.4 - resolution: "negotiator@npm:0.6.4" - checksum: 10c0/3e677139c7fb7628a6f36335bf11a885a62c21d5390204590a1a214a5631fcbe5ea74ef6a610b60afe84b4d975cbe0566a23f20ee17c77c73e74b80032108dea - languageName: node - linkType: hard - "negotiator@npm:^1.0.0": version: 1.0.0 resolution: "negotiator@npm:1.0.0" @@ -23374,12 +23186,12 @@ __metadata: languageName: node linkType: hard -"node-abi@npm:4.12.0": - version: 4.12.0 - resolution: "node-abi@npm:4.12.0" +"node-abi@npm:4.24.0": + version: 4.24.0 + resolution: "node-abi@npm:4.24.0" dependencies: semver: "npm:^7.6.3" - checksum: 10c0/78a0697b1ea7da95bee5465d92772a883fb829ae01e89cf2b60c2af79bf784e422cdde9cc2f15684d5bafb9a0ac3d8e16292520107dc36eff30c541e23ac9fb7 + checksum: 10c0/9bf9f4e79c875b98f8026f2ad80150b2d5077f48529444232c9574cfd82e45d42a3ab2dcf6fb374cf7775becbf58e7c1b8704596ad3bef27cdeab7bc93eca7a3 languageName: node linkType: hard @@ -23419,7 +23231,7 @@ __metadata: languageName: node linkType: hard -"node-api-version@npm:^0.2.0": +"node-api-version@npm:^0.2.1": version: 0.2.1 resolution: "node-api-version@npm:0.2.1" dependencies: @@ -23478,6 +23290,26 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:^11.2.0": + version: 11.5.0 + resolution: "node-gyp@npm:11.5.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^14.0.3" + nopt: "npm:^8.0.0" + proc-log: "npm:^5.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.4.3" + tinyglobby: "npm:^0.2.12" + which: "npm:^5.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/31ff49586991b38287bb15c3d529dd689cfc32f992eed9e6997b9d712d5d21fe818a8b1bbfe3b76a7e33765c20210c5713212f4aa329306a615b87d8a786da3a + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 11.2.0 resolution: "node-gyp@npm:11.2.0" @@ -23519,17 +23351,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^6.0.0": - version: 6.0.0 - resolution: "nopt@npm:6.0.0" - dependencies: - abbrev: "npm:^1.0.0" - bin: - nopt: bin/nopt.js - checksum: 10c0/837b52c330df16fcaad816b1f54fec6b2854ab1aa771d935c1603fbcf9b023bb073f1466b1b67f48ea4dce127ae675b85b9d9355700e9b109de39db490919786 - languageName: node - linkType: hard - "nopt@npm:^8.0.0": version: 8.1.0 resolution: "nopt@npm:8.1.0" @@ -24001,15 +23822,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: "npm:^3.0.0" - checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 - languageName: node - linkType: hard - "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -24683,13 +24495,6 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^2.0.1": - version: 2.0.1 - resolution: "proc-log@npm:2.0.1" - checksum: 10c0/701c501429775ce34cec28ef6a1c976537274b42917212fb8a5975ebcecb0a85612907fd7f99ff28ff4c2112bb84a0f4322fc9b9e1e52a8562fcbb1d5b3ce608 - languageName: node - linkType: hard - "proc-log@npm:^5.0.0": version: 5.0.0 resolution: "proc-log@npm:5.0.0" @@ -24718,13 +24523,6 @@ __metadata: languageName: node linkType: hard -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 10c0/d179d148d98fbff3d815752fa9a08a87d3190551d1420f17c4467f628214db12235ae068d98cd001f024453676d8985af8f28f002345646c4ece4600a79620bc - languageName: node - linkType: hard - "promise-limit@npm:^2.7.0": version: 2.7.0 resolution: "promise-limit@npm:2.7.0" @@ -27085,6 +26883,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.7.2, semver@npm:^7.7.2": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea + languageName: node + linkType: hard + "semver@npm:^5.5.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -27103,7 +26910,16 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.2, semver@npm:^7.7.3": +"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": + version: 7.7.1 + resolution: "semver@npm:7.7.1" + bin: + semver: bin/semver.js + checksum: 10c0/fd603a6fb9c399c6054015433051bdbe7b99a940a8fb44b85c2b524c4004b023d7928d47cb22154f8d054ea7ee8597f586605e05b52047f048278e4ac56ae958 + languageName: node + linkType: hard + +"semver@npm:^7.6.2, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -27526,17 +27342,6 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "socks-proxy-agent@npm:7.0.0" - dependencies: - agent-base: "npm:^6.0.2" - debug: "npm:^4.3.3" - socks: "npm:^2.6.2" - checksum: 10c0/b859f7eb8e96ec2c4186beea233ae59c02404094f3eb009946836af27d6e5c1627d1975a69b4d2e20611729ed543b6db3ae8481eb38603433c50d0345c987600 - languageName: node - linkType: hard - "socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" @@ -27548,7 +27353,7 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.6.2, socks@npm:^2.8.2, socks@npm:^2.8.3": +"socks@npm:^2.8.2, socks@npm:^2.8.3": version: 2.8.6 resolution: "socks@npm:2.8.6" dependencies: @@ -27639,15 +27444,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^9.0.0": - version: 9.0.1 - resolution: "ssri@npm:9.0.1" - dependencies: - minipass: "npm:^3.1.1" - checksum: 10c0/c5d153ce03b5980d683ecaa4d805f6a03d8dc545736213803e168a1907650c46c08a4e5ce6d670a0205482b35c35713d9d286d9133bdd79853a406e22ad81f04 - languageName: node - linkType: hard - "stack-trace@npm:0.0.x": version: 0.0.10 resolution: "stack-trace@npm:0.0.10" @@ -28260,7 +28056,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.2.1": +"tar@npm:^6.0.5, tar@npm:^6.1.12": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -28969,7 +28765,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.0, typescript@npm:^5.4.3, typescript@npm:^5.6.2, typescript@npm:^5.8.2": +"typescript@npm:^5.0.0, typescript@npm:^5.6.2, typescript@npm:^5.8.2": version: 5.9.3 resolution: "typescript@npm:5.9.3" bin: @@ -28989,7 +28785,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A^5.6.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": +"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin, typescript@patch:typescript@npm%3A^5.6.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: @@ -29009,7 +28805,16 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^1.0.35, ua-parser-js@npm:^1.0.37": +"ua-parser-js@npm:^1.0.35": + version: 1.0.40 + resolution: "ua-parser-js@npm:1.0.40" + bin: + ua-parser-js: script/cli.js + checksum: 10c0/2b6ac642c74323957dae142c31f72287f2420c12dced9603d989b96c132b80232779c429b296d7de4012ef8b64e0d8fadc53c639ef06633ce13d785a78b5be6c + languageName: node + linkType: hard + +"ua-parser-js@npm:^1.0.37": version: 1.0.41 resolution: "ua-parser-js@npm:1.0.41" bin: @@ -29127,15 +28932,6 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^2.0.0": - version: 2.0.1 - resolution: "unique-filename@npm:2.0.1" - dependencies: - unique-slug: "npm:^3.0.0" - checksum: 10c0/55d95cd670c4a86117ebc34d394936d712d43b56db6bc511f9ca00f666373818bf9f075fb0ab76bcbfaf134592ef26bb75aad20786c1ff1ceba4457eaba90fb8 - languageName: node - linkType: hard - "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -29145,15 +28941,6 @@ __metadata: languageName: node linkType: hard -"unique-slug@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-slug@npm:3.0.0" - dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10c0/617240eb921af803b47d322d75a71a363dacf2e56c29ae5d1404fad85f64f4ec81ef10ee4fd79215d0202cbe1e5a653edb0558d59c9c81d3bd538c2d58e4c026 - languageName: node - linkType: hard - "unique-slug@npm:^5.0.0": version: 5.0.0 resolution: "unique-slug@npm:5.0.0" @@ -29931,7 +29718,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^2.0.1, which@npm:^2.0.2": +"which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: